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.
/*---------------------------------------------------------------------------------------------
languageModels.ts ×93
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SequencerByKey, timeout } from '../../../../base/common/async.js';
import { VSBuffer } from '../../../../base/common/buffer.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { IStringDictionary } from '../../../../base/common/collections.js';
import { CancellationError, getErrorMessage, isCancellationError } from '../../../../base/common/errors.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { hash } from '../../../../base/common/hash.js';
import { Iterable } from '../../../../base/common/iterator.js';
import { IJSONSchema, TypeFromJsonSchema } from '../../../../base/common/jsonSchema.js';
import { DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { IObservable, observableValue } from '../../../../base/common/observable.js';
import { equals } from '../../../../base/common/objects.js';
import Severity from '../../../../base/common/severity.js';
import { format, isFalsyOrWhitespace } from '../../../../base/common/strings.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { IAction, SubmenuAction } from '../../../../base/common/actions.js';
import { isObject, isString } from '../../../../base/common/types.js';
import { Schemas } from '../../../../base/common/network.js';
import { URI } from '../../../../base/common/uri.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { localize } from '../../../../nls.js';
import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { INotificationService, NeverShowAgainScope } from '../../../../platform/notification/common/notification.js';
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { IProductService } from '../../../../platform/product/common/productService.js';
import { asJson, IRequestService } from '../../../../platform/request/common/request.js';
import { IQuickInputService, IQuickPickItem, QuickInputHideReason } from '../../../../platform/quickinput/common/quickInput.js';
import { ISecretStorageService } from '../../../../platform/secrets/common/secrets.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
import { ExtensionsRegistry } from '../../../services/extensions/common/extensionsRegistry.js';
import { ChatContextKeys } from './actions/chatContextKeys.js';
import { ChatAgentLocation } from './constants.js';
import { ILanguageModelsProviderGroup, ILanguageModelsConfigurationService } from './languageModelsConfiguration.js';
/**
* Vendor id used for the built-in GitHub Copilot language model provider. Treated as the default
* vendor across the chat stack (see `ILanguageModelProviderDescriptor.isDefault`).
*/
export const COPILOT_VENDOR_ID = 'copilot';
/** Whether a missing model is conclusively absent from a vendor's live model list. Empty Copilot results remain transient while token-backed discovery completes. */
export function isLanguageModelVendorAbsenceConclusive(vendor: string, hasLiveModels: boolean, hasResolved: boolean): boolean {
}
/**
* Vendor ids of the BYOK language-model providers that ship in-built with the GitHub Copilot Chat
* extension. Each provider's vendor id is `providerName.toLowerCase()` (see
* `extensions/copilot/src/extension/byok/vscode-node/*Provider.ts`). This list is intentionally
* hardcoded: the in-built provider set is stable and known ahead of time, which lets us report these
* providers by name while bucketing every other (third-party) provider as `3p-extension`.
*/
const BUILT_IN_BYOK_VENDOR_IDS = new Set<string>([
'openai',
'anthropic',
'gemini',
'ollama',
'openrouter',
'azure',
'xai',
'customoai',
'customendpoint',
]);
/**
* Bucket reported for any non-Copilot provider that is not an in-built BYOK provider, i.e. a model
* contributed by a third-party extension. We never report the third-party vendor id directly to avoid
* logging potentially identifying values.
*/
export const THIRD_PARTY_PROVIDER_TELEMETRY_NAME = '3p-extension';
const BUILT_IN_BYOK_EXTENSION_IDS = [
'github.copilot-chat',
'github.copilot',
];
/**
* Normalizes a non-Copilot model vendor into a non-identifying provider name suitable for telemetry:
* the in-built BYOK vendor id (e.g. `openai`, `ollama`) when contributed by the built-in Copilot
* extensions, or {@link THIRD_PARTY_PROVIDER_TELEMETRY_NAME} otherwise. Returns `undefined` for the
* first-party Copilot vendor (or no vendor) so callers skip logging first-party usage.
*/
export function getByokProviderTelemetryName(vendor: string | undefined, extension: ExtensionIdentifier | undefined): string | undefined {
}
if (BUILT_IN_BYOK_VENDOR_IDS.has(vendor) && extension && BUILT_IN_BYOK_EXTENSION_IDS.some(id => ExtensionIdentifier.equals(extension, id))) {
languageModels.ts ×2
}
}
export const enum ChatMessageRole {
System,
User,
Assistant,
}
export enum LanguageModelPartAudience {
Assistant = 0,
User = 1,
Extension = 2,
}
export interface IChatMessageTextPart {
type: 'text';
value: string;
audience?: LanguageModelPartAudience[];
}
export interface IChatMessageImagePart {
type: 'image_url';
value: IChatImageURLPart;
}
export interface IChatMessageThinkingPart {
type: 'thinking';
value: string | string[];
id?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
metadata?: { readonly [key: string]: any };
}
export interface IChatMessageDataPart {
type: 'data';
mimeType: string;
data: VSBuffer;
audience?: LanguageModelPartAudience[];
}
export interface IChatImageURLPart {
/**
* The image's MIME type (e.g., "image/png", "image/jpeg").
*/
mimeType: ChatImageMimeType;
/**
* The raw binary data of the image, encoded as a Uint8Array. Note: do not use base64 encoding. Maximum image size is 5MB.
*/
data: VSBuffer;
}
/**
* Enum for supported image MIME types.
*/
export enum ChatImageMimeType {
PNG = 'image/png',
JPEG = 'image/jpeg',
GIF = 'image/gif',
WEBP = 'image/webp',
BMP = 'image/bmp',
}
/**
* Specifies the detail level of the image.
*/
export enum ImageDetailLevel {
Low = 'low',
High = 'high'
}
export interface IChatMessageToolResultPart {
type: 'tool_result';
toolCallId: string;
value: (IChatResponseTextPart | IChatResponsePromptTsxPart | IChatResponseDataPart)[];
isError?: boolean;
}
export type IChatMessagePart = IChatMessageTextPart | IChatMessageToolResultPart | IChatResponseToolUsePart | IChatMessageImagePart | IChatMessageDataPart | IChatMessageThinkingPart;
export interface IChatMessage {
readonly name?: string | undefined;
readonly role: ChatMessageRole;
readonly content: IChatMessagePart[];
}
export interface IChatResponseTextPart {
type: 'text';
value: string;
audience?: LanguageModelPartAudience[];
}
export interface IChatResponsePromptTsxPart {
type: 'prompt_tsx';
value: unknown;
}
export interface IChatResponseDataPart {
type: 'data';
mimeType: string;
data: VSBuffer;
audience?: LanguageModelPartAudience[];
}
export interface IChatResponseToolUsePart {
type: 'tool_use';
name: string;
toolCallId: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
parameters: any;
}
export interface IChatResponseThinkingPart {
type: 'thinking';
value: string | string[];
id?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
metadata?: { readonly [key: string]: any };
}
export interface IChatResponsePullRequestPart {
type: 'pullRequest';
uri: URI;
title: string;
description: string;
author: string;
linkTag: string;
}
export type IChatResponsePart = IChatResponseTextPart | IChatResponseToolUsePart | IChatResponseDataPart | IChatResponseThinkingPart;
export type IExtendedChatResponsePart = IChatResponsePullRequestPart;
export interface ILanguageModelConfigurationSchema extends IJSONSchema {
properties?: {
[key: string]: IJSONSchema & {
/** When set to `'navigation'`, the property is shown as a primary action in the model picker. */
group?: string;
/** Labels for enum values. If provided, these are shown instead of the raw enum values. */
enumItemLabels?: string[];
};
};
}
export interface ILanguageModelChatMetadata {
readonly extension: ExtensionIdentifier;
readonly name: string;
readonly id: string;
readonly vendor: string;
readonly version: string;
readonly tooltip?: string;
readonly detail?: string;
readonly multiplierNumeric?: number;
readonly isBYOK?: boolean;
readonly pricing?: string;
readonly inputCost?: number;
readonly cacheCost?: number;
readonly cacheWriteCost?: number;
readonly outputCost?: number;
readonly longContextInputCost?: number;
readonly longContextCacheCost?: number;
readonly longContextCacheWriteCost?: number;
readonly longContextOutputCost?: number;
readonly priceCategory?: string;
readonly category?: string;
readonly family: string;
readonly maxInputTokens: number;
readonly maxOutputTokens: number;
readonly isDefaultForLocation: { [K in ChatAgentLocation]?: boolean };
readonly isUserSelectable?: boolean;
readonly statusIcon?: ThemeIcon;
readonly auth?: {
readonly providerLabel: string;
readonly accountLabel?: string;
};
readonly capabilities?: {
readonly vision?: boolean;
readonly toolCalling?: boolean;
readonly agentMode?: boolean;
readonly editTools?: ReadonlyArray<string>;
};
/**
* When set, this model is only shown in the model picker for the specified chat session type.
* Models with this property are excluded from the general model picker and only appear
* when the user is in a session matching this type.
*/
readonly targetChatSessionType?: string;
/**
* Optional grouping hint for the model picker. When set, the picker buckets this model
* under a sub-group within its vendor, identified by this vendor id — e.g. agent-host models,
* which all share one vendor, grouped by their upstream provider — instead of a single
* vendor-wide bucket. The display name is resolved from the vendor registry
* ({@link ILanguageModelsService.getVendors}), the same source used for every other vendor.
* Presentation-only; it does not affect model selection or routing.
*/
readonly modelGroup?: { readonly id: string };
/**
* For an agent-host copy of an extension-provided BYOK model, the identifier the
* original model is registered under in the renderer's LM service
* (`toModelIdentifier(vendor, group, id)` — `<vendor>/<group>/<id>` or `<vendor>/<id>`).
* This is exactly the id the "Manage Models" view keys visibility by; it is carried
* across the agent-host bridge and surfaced here so the model picker can honour the
* model's visibility toggle. Absent for native agent-host models and non-agent-host
* models.
*/
readonly byokModelIdentifier?: string;
/**
* An optional JSON schema describing the per-model configuration options.
* Used to validate user-provided per-model configuration in `chatLanguageModels.json`.
*/
readonly configurationSchema?: ILanguageModelConfigurationSchema;
/**
* Optional warning text to display in the model picker hover as a warning banner.
* The keys are warning categories (e.g. "data_retention") and the values are markdown strings.
*/
readonly warningText?: IStringDictionary<string>;
/**
* Optional promotional information for this model. Positive discounts surface
* promotional UI; non-positive discounts only feature the model in the picker.
*/
readonly promo?: {
readonly id: string;
readonly discountPercent: number;
readonly endsAt: string;
readonly message: string;
};
}
export namespace ILanguageModelChatMetadata {
export function suitableForAgentMode(metadata: ILanguageModelChatMetadata): boolean {
const supportsToolsAgent = typeof metadata.capabilities?.agentMode === 'undefined' || metadata.capabilities.agentMode;
runSubagentTool.ts ×4
return supportsToolsAgent && !!metadata.capabilities?.toolCalling;
}
export function asQualifiedName(metadata: ILanguageModelChatMetadata): string {
}
export function matchesQualifiedName(name: string, metadata: ILanguageModelChatMetadata): boolean {
if (metadata.vendor === COPILOT_VENDOR_ID && name === metadata.name) {
return true;
}
return name === asQualifiedName(metadata);
}
export function hasPromoDiscount(metadata: ILanguageModelChatMetadata): metadata is ILanguageModelChatMetadata & { readonly promo: NonNullable<ILanguageModelChatMetadata['promo']> } {
return !!metadata.promo && metadata.promo.discountPercent > 0;
}
/**
* Documentation link explaining how Auto model selection works.
* NOTE: Also defined in extensions/copilot/src/extension/conversation/common/languageModelAccess.ts — keep in sync.
*/
export const autoModelSelectionDocsUrl = 'https://docs.github.com/en/copilot/concepts/models/auto-model-selection';
/**
* Builds the shared description shown for the Auto model, rendered as Markdown
* (it contains a "Learn More" link). The discount sentence is only included
* when a positive discount is provided.
*
* @param discountPercent Whole-number percentage (e.g. `10` for 10%). When
* omitted or not positive, the discount sentence is left out entirely.
*/
export function getAutoModelDescription(discountPercent?: number): string {
const base = localize('autoModel.description', "Auto routes based on your task and real-time system health and model performance.");
const learnMore = localize('autoModel.learnMore', "[Learn More]({0})", autoModelSelectionDocsUrl);
if (typeof discountPercent === 'number' && discountPercent > 0) {
const discount = localize('autoModel.discount', "Models routed via auto receive a {0}% discount.", discountPercent);
return `${base} ${discount} ${learnMore}`;
}
return `${base} ${learnMore}`;
}
/**
* The "Manage Models" identifier that an agent-host copy of an extension-provided
* BYOK model is toggled under, or `undefined` when the model is not such a copy.
*
* Agent-host BYOK models make a round trip that rewrites their id (the node agent host
* re-advertises the extension model under the agent-host vendor). Their original LM
* service identifier — `toModelIdentifier(vendor, group, id)`, i.e. `<vendor>/<group>/<id>`
* or `<vendor>/<id>`, which is what the Manage Models view stores when hiding the model —
* is carried across the bridge and surfaced on {@link ILanguageModelChatMetadata.byokModelIdentifier}.
* This returns it, so callers can match the copy against the user's visibility toggles.
*
* Returns `undefined` for models that are not agent-host BYOK copies (native harness
* models and non-agent-host models), which are matched by their own identifier instead.
*/
export function getAgentHostByokManageModelsIdentifier(metadata: ILanguageModelChatMetadata): string | undefined {
}
export interface ILanguageModelChatResponse {
stream: AsyncIterable<IChatResponsePart | IChatResponsePart[]>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result: Promise<any>;
}
export async function getTextResponseFromStream(response: ILanguageModelChatResponse): Promise<string> {
let responseText = '';
const streaming = (async () => {
if (!response?.stream) {
return;
}
for await (const part of response.stream) {
if (Array.isArray(part)) {
for (const item of part) {
if (item.type === 'text') {
responseText += item.value;
}
}
} else if (part.type === 'text') {
responseText += part.value;
}
}
})();
try {
await Promise.all([response.result, streaming]);
return responseText;
} catch (err) {
if (responseText) {
return responseText;
}
throw err;
}
}
export interface ILanguageModelChatProvider {
readonly onDidChange: Event<void>;
provideLanguageModelChatInfo(options: ILanguageModelChatInfoOptions, token: CancellationToken): Promise<ILanguageModelChatMetadataAndIdentifier[]>;
sendChatRequest(modelId: string, messages: IChatMessage[], from: ExtensionIdentifier | undefined, options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise<ILanguageModelChatResponse>;
provideTokenCount(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise<number>;
}
export interface ILanguageModelChat {
metadata: ILanguageModelChatMetadata;
sendChatRequest(messages: IChatMessage[], from: ExtensionIdentifier | undefined, options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise<ILanguageModelChatResponse>;
provideTokenCount(message: string | IChatMessage, token: CancellationToken): Promise<number>;
}
export interface ILanguageModelChatSelector {
readonly name?: string;
readonly id?: string;
readonly vendor?: string;
readonly version?: string;
readonly family?: string;
readonly tokens?: number;
readonly extension?: ExtensionIdentifier;
}
export function isILanguageModelChatSelector(value: unknown): value is ILanguageModelChatSelector {
if (typeof value !== 'object' || value === null) {
return false;
}
const obj = value as Record<string, unknown>;
return (
(obj.name === undefined || typeof obj.name === 'string') &&
(obj.id === undefined || typeof obj.id === 'string') &&
(obj.vendor === undefined || typeof obj.vendor === 'string') &&
(obj.version === undefined || typeof obj.version === 'string') &&
(obj.family === undefined || typeof obj.family === 'string') &&
(obj.tokens === undefined || typeof obj.tokens === 'number') &&
(obj.extension === undefined || typeof obj.extension === 'object')
);
}
export const ILanguageModelsService = createDecorator<ILanguageModelsService>('ILanguageModelsService');
export interface ILanguageModelChatMetadataAndIdentifier {
metadata: ILanguageModelChatMetadata;
identifier: string;
}
export interface ILanguageModelChatInfoOptions {
readonly group?: string;
readonly silent: boolean;
readonly configuration?: IStringDictionary<unknown>;
}
export interface ILanguageModelChatRequestOptions {
readonly modelOptions?: IStringDictionary<unknown>;
readonly configuration?: IStringDictionary<unknown>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly [name: string]: any;
}
export interface ILanguageModelsGroup {
readonly group?: ILanguageModelsProviderGroup;
readonly modelIdentifiers: string[];
readonly status?: {
readonly message: string;
readonly severity: Severity;
};
}
export interface ILanguageModelsService {
readonly _serviceBrand: undefined;
readonly onDidChangeLanguageModelVendors: Event<readonly string[]>;
readonly onDidChangeLanguageModels: Event<string>;
getLanguageModelIds(): string[];
getVendors(): ILanguageModelProviderDescriptor[];
lookupLanguageModel(modelId: string): ILanguageModelChatMetadata | undefined;
/**
* 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)".
*/
lookupLanguageModelByQualifiedName(qualifiedName: string): ILanguageModelChatMetadataAndIdentifier | undefined;
getLanguageModelGroups(vendor: string): ILanguageModelsGroup[];
/**
* Returns true if the given vendor's provider has completed at least one
* model resolution since registration. A `false` result indicates the
* vendor is still in a startup/reload race where its model list isn't yet
* authoritative — callers can fall back to a cached list in that case.
*/
hasResolvedVendor(vendor: string): boolean;
/**
* Given a selector, returns a list of model identifiers
* @param selector The selector to lookup for language models. If the selector is empty, all language models are returned.
*/
selectLanguageModels(selector: ILanguageModelChatSelector): Promise<string[]>;
registerLanguageModelProvider(vendor: string, provider: ILanguageModelChatProvider): IDisposable;
deltaLanguageModelChatProviderDescriptors(added: IUserFriendlyLanguageModel[], removed: IUserFriendlyLanguageModel[]): void;
sendChatRequest(modelId: string, from: ExtensionIdentifier | undefined, messages: IChatMessage[], options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise<ILanguageModelChatResponse>;
computeTokenLength(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise<number>;
/**
* Returns the resolved per-model configuration for the given model identifier.
* Includes schema defaults with user overrides applied on top.
* Returns undefined if the model has no configuration schema and no user config.
*/
getModelConfiguration(modelId: string): IStringDictionary<unknown> | undefined;
/**
* Updates the per-model configuration for the given model.
* Merges the provided values into the existing configuration.
*/
setModelConfiguration(modelId: string, values: IStringDictionary<unknown>): Promise<void>;
/**
* Returns actions for configuring the given model based on its configuration schema.
* For enum properties, returns submenu actions with checkable values.
* Returns an empty array if the model has no configuration schema.
*/
getModelConfigurationActions(modelId: string): IAction[];
addLanguageModelsProviderGroup(name: string, vendorId: string, configuration: IStringDictionary<unknown> | undefined): Promise<void>;
removeLanguageModelsProviderGroup(vendorId: string, providerGroupName: string): Promise<void>;
configureLanguageModelsProviderGroup(vendorId: string, name?: string): Promise<void>;
renameLanguageModelsProviderGroup(vendorId: string, providerGroupName: string): Promise<void>;
updateLanguageModelsProviderGroupApiKey(vendorId: string, providerGroupName: string): Promise<void>;
addLanguageModelsProviderGroupModel(vendorId: string, providerGroupName: string): Promise<void>;
openLanguageModelsProviderGroupSettings(vendorId: string, providerGroupName: string): Promise<void>;
/**
* Opens the language models configuration file and navigates to
* or creates the per-model configuration for the given model.
*/
configureModel(modelId: string): Promise<void>;
migrateLanguageModelsProviderGroup(languageModelsProviderGroup: ILanguageModelsProviderGroup): Promise<void>;
/**
* Returns the most recently used model identifiers, ordered by most-recent-first.
* @param maxCount Maximum number of entries to return (default 7).
*/
getRecentlyUsedModelIds(): string[];
/**
* Records that a model was used, updating the recently used list.
*/
addToRecentlyUsedList(modelIdentifier: string): void;
/**
* Clears the recently used model list.
*/
clearRecentlyUsedList(): void;
/**
* Returns the pinned model identifiers, in the order they were pinned.
*/
getPinnedModelIds(): string[];
/**
* Pins a model so it appears in the pinned section of the model picker.
*/
pinModel(modelIdentifier: string): void;
/**
* Unpins a model, removing it from the pinned section.
*/
unpinModel(modelIdentifier: string): void;
/**
* Returns whether the given model is pinned.
*/
isModelPinned(modelIdentifier: string): boolean;
/**
* Fires when the pinned models list changes.
*/
readonly onDidChangePinnedModels: Event<void>;
/**
* Returns whether the given model is hidden from the chat model picker.
*/
isModelHidden(modelIdentifier: string): boolean;
/**
* Returns whether every resolved model in the given (vendor, groupName)
* bucket is hidden from the chat model picker.
*/
isGroupHidden(vendor: string, groupName: string): boolean;
/**
* Hide or show a single model in the chat model picker.
*/
setModelHidden(modelIdentifier: string, hidden: boolean): void;
/**
* Hide or show every model in a (vendor, groupName) bucket.
*/
setGroupHidden(vendor: string, groupName: string, hidden: boolean): void;
/**
* Returns the persisted per-model hidden identifiers.
*/
getHiddenModelIds(): string[];
/**
* Fires when any model or group visibility state changes.
*/
readonly onDidChangeModelVisibility: Event<void>;
/**
* Returns the models from the control manifest,
* separated into free and paid tiers.
*/
getModelsControlManifest(): IModelsControlManifest;
/**
* Fires when models control manifest changes.
*/
readonly onDidChangeModelsControlManifest: Event<IModelsControlManifest>;
/**
* Observable map of restricted chat participant names to allowed extension publisher/IDs.
* Fetched from the chat control manifest.
*/
readonly restrictedChatParticipants: IObservable<{ [name: string]: string[] }>;
}
export interface IModelControlEntry {
readonly label: string;
readonly featured?: boolean;
readonly minVSCodeVersion?: string;
readonly exists: boolean;
}
export interface IModelsControlManifest {
readonly free: IStringDictionary<IModelControlEntry>;
readonly paid: IStringDictionary<IModelControlEntry>;
}
const languageModelChatProviderType = {
type: 'object',
required: ['vendor', 'displayName'],
properties: {
vendor: {
type: 'string',
description: localize('vscode.extension.contributes.languageModels.vendor', "A globally unique vendor of language model chat provider.")
},
displayName: {
type: 'string',
description: localize('vscode.extension.contributes.languageModels.displayName', "The display name of the language model chat provider.")
},
configuration: {
type: 'object',
description: localize('vscode.extension.contributes.languageModels.configuration', "Configuration options for the language model chat provider."),
anyOf: [
{
$ref: 'http://json-schema.org/draft-07/schema#'
},
{
properties: {
properties: {
type: 'object',
additionalProperties: {
$ref: 'http://json-schema.org/draft-07/schema#',
properties: {
secret: {
type: 'boolean',
description: localize('vscode.extension.contributes.languageModels.configuration.secret', "Whether the property is a secret.")
}
}
}
},
additionalProperties: {
$ref: 'http://json-schema.org/draft-07/schema#',
properties: {
secret: {
type: 'boolean',
description: localize('vscode.extension.contributes.languageModels.configuration.secret', "Whether the property is a secret.")
}
}
}
}
}
]
},
managementCommand: {
type: 'string',
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."),
deprecated: true,
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.")
},
deprecation: {
type: 'object',
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."),
properties: {
link: {
type: 'string',
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.")
}
}
},
when: {
type: 'string',
description: localize('vscode.extension.contributes.languageModels.when', "Condition which must be true to show this language model chat provider in the Manage Models list.")
}
}
} as const satisfies IJSONSchema;
export type IUserFriendlyLanguageModel = Omit<TypeFromJsonSchema<typeof languageModelChatProviderType>, 'deprecation'> & {
/**
* Marks a provider as deprecated. The Manage Models view renders a link
* (pointing to a replacement, e.g. a `vscode:extension/<publisher>.<name>` URI)
* next to the provider name. Optional so existing provider descriptors are unaffected.
*/
readonly deprecation?: { readonly link?: string };
};
export interface ILanguageModelProviderDescriptor extends IUserFriendlyLanguageModel {
readonly isDefault: boolean;
}
/**
* Resolves a provider `deprecation.link` for opening inside the current build. Contributions point
* at the replacement extension with a stable `vscode:extension/<id>` URI, but the URL service only
* routes URIs whose scheme matches this build's `urlProtocol` (e.g. `code-oss`, `vscode-insiders`).
* The `vscode:` scheme is therefore rewritten to the current protocol so the extensions URL handler
* opens the extension; without this the opener falls back to treating the URI as a (non-existent)
* file resource and fails. Other schemes (http(s), command) are returned unchanged.
*/
export function resolveProviderDeprecationLink(link: string, urlProtocol: string | undefined): URI {
return uri.scheme === Schemas.vscode && urlProtocol ? uri.with({ scheme: urlProtocol }) : uri;
}
export const languageModelChatProviderExtensionPoint = ExtensionsRegistry.registerExtensionPoint<IUserFriendlyLanguageModel | IUserFriendlyLanguageModel[]>({
extensionPoint: 'languageModelChatProviders',
jsonSchema: {
description: localize('vscode.extension.contributes.languageModelChatProviders', "Contribute language model chat providers of a specific vendor."),
oneOf: [
languageModelChatProviderType,
{
type: 'array',
items: languageModelChatProviderType
}
]
},
activationEventsGenerator: function* (contribs: readonly IUserFriendlyLanguageModel[]) {
for (const contrib of contribs) {
yield `onLanguageModelChatProvider:${contrib.vendor}`;
}
}
const CHAT_MODEL_RECENTLY_USED_STORAGE_KEY = 'chatModelRecentlyUsed';
const CHAT_MODEL_PINNED_STORAGE_KEY = 'chatModelPinned';
const CHAT_MODEL_VISIBILITY_STORAGE_KEY = 'chatModelVisibility';
/**
* The identifier for the Auto model which dynamically routes to the best backend.
* Auto should never appear in user-curated lists (MRU, pinned).
*/
const AUTO_MODEL_IDENTIFIER = 'copilot/auto';
export function isAutoLanguageModel(model: ILanguageModelChatMetadataAndIdentifier | undefined): boolean {
return model?.metadata.id === 'auto' || model?.identifier === AUTO_MODEL_IDENTIFIER;
}
const CHAT_PARTICIPANT_NAME_REGISTRY_STORAGE_KEY = 'chat.participantNameRegistry';
const CHAT_MODELS_CONTROL_STORAGE_KEY = 'chat.modelsControl';
interface IChatControlResponse {
readonly version: number;
readonly restrictedChatParticipants: { [name: string]: string[] };
readonly models?: {
readonly free?: Record<string, { readonly label: string; readonly featured?: boolean }>;
readonly paid?: Record<string, { readonly label: string; readonly featured?: boolean; readonly minVSCodeVersion?: string }>;
};
}
/**
* Builds the per-model configuration submenu actions from a model's
* {@link ILanguageModelConfigurationSchema}. The current value is read from
* `currentConfig` and selections are routed through `setValue`, allowing the
* caller to decide whether changes apply globally or to a per-editor override.
*/
export function createModelConfigurationActions(
currentConfig: IStringDictionary<unknown>,
setValue: (key: string, value: unknown) => void,
): IAction[] {
if (!schema?.properties) {
}
const actions: IAction[] = [];
for (const [key, propSchema] of Object.entries(schema.properties)) {
if (!propSchema.enum || !Array.isArray(propSchema.enum) || propSchema.enum.length < 1) {
continue;
}
const currentValue = currentConfig[key] ?? propSchema.default;
const label = (typeof propSchema.title === 'string' ? propSchema.title : undefined)
?? key.replace(/([a-z])([A-Z])/g, '$1 $2')
.replace(/^./, s => s.toUpperCase());
const defaultValue = propSchema.default;
const enumItemLabels = propSchema.enumItemLabels;
const enumDescriptions = propSchema.enumDescriptions;
const enumActions: IAction[] = propSchema.enum.map((value: unknown, index: number) => {
const itemLabel = enumItemLabels?.[index] ?? String(value);
const displayLabel = value === defaultValue ? localize('models.enumDefault', "{0} (default)", itemLabel) : itemLabel;
const tooltip = enumDescriptions?.[index] ?? '';
return {
id: `configureModel.${key}.${value}`,
label: displayLabel,
class: undefined,
enabled: true,
tooltip,
checked: currentValue === value,
run: () => setValue(key, value)
};
});
actions.push(new SubmenuAction(`configureModel.${key}`, label, enumActions));
}
return actions;
}
export class LanguageModelsService implements ILanguageModelsService {
private static SECRET_KEY_PREFIX = 'chat.lm.secret.';
private static SECRET_INPUT = '${input:{0}}';
readonly _serviceBrand: undefined;
private readonly _store = new DisposableStore();
private readonly _providers = new Map<string, ILanguageModelChatProvider>();
private readonly _vendors = new Map<string, ILanguageModelProviderDescriptor>();
/** Vendors for which a deprecation notice has already been shown this session. */
private readonly _deprecationNoticeShownVendors = new Set<string>();
private readonly _onDidChangeLanguageModelVendors = this._store.add(new Emitter<string[]>());
readonly onDidChangeLanguageModelVendors = this._onDidChangeLanguageModelVendors.event;
private readonly _modelsGroups = new Map<string, ILanguageModelsGroup[]>();
private readonly _modelCache = new Map<string, ILanguageModelChatMetadata>();
private readonly _resolveLMSequencer = new SequencerByKey<string>();
private readonly _modelConfigurations = new Map<string, IStringDictionary<unknown>>();
private readonly _hasUserSelectableModels: IContextKey<boolean>;
private readonly _hasNonCopilotUserSelectableModels: IContextKey<boolean>;
private readonly _onLanguageModelChange = this._store.add(new Emitter<string>());
readonly onDidChangeLanguageModels: Event<string> = this._onLanguageModelChange.event;
private _recentlyUsedModelIds: string[] = [];
private _pinnedModelIds: string[] = [];
private _hiddenModelIds = new Set<string>();
private readonly _onDidChangeModelsControlManifest = this._store.add(new Emitter<IModelsControlManifest>());
readonly onDidChangeModelsControlManifest = this._onDidChangeModelsControlManifest.event;
private readonly _onDidChangePinnedModels = this._store.add(new Emitter<void>());
readonly onDidChangePinnedModels = this._onDidChangePinnedModels.event;
private readonly _onDidChangeModelVisibility = this._store.add(new Emitter<void>());
readonly onDidChangeModelVisibility = this._onDidChangeModelVisibility.event;
private _modelsControlManifest: IModelsControlManifest = { free: {}, paid: {} };
private _modelsControlRawResponse: IChatControlResponse['models'] | undefined;
private _chatControlUrl: string | undefined;
private _chatControlDisposed = false;
private readonly _restrictedChatParticipants = observableValue<{ [name: string]: string[] }>(this, Object.create(null));
readonly restrictedChatParticipants: IObservable<{ [name: string]: string[] }> = this._restrictedChatParticipants;
constructor(
@IExtensionService private readonly _extensionService: IExtensionService,
languageModels.ts ×19
@ILogService private readonly _logService: ILogService,
@IStorageService private readonly _storageService: IStorageService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@ILanguageModelsConfigurationService private readonly _languageModelsConfigurationService: ILanguageModelsConfigurationService,
@IQuickInputService private readonly _quickInputService: IQuickInputService,
@ISecretStorageService private readonly _secretStorageService: ISecretStorageService,
@IProductService private readonly _productService: IProductService,
@IRequestService private readonly _requestService: IRequestService,
@INotificationService private readonly _notificationService: INotificationService,
@IOpenerService private readonly _openerService: IOpenerService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
) {
this._hasUserSelectableModels = ChatContextKeys.languageModelsAreUserSelectable.bindTo(_contextKeyService);
this._hasNonCopilotUserSelectableModels = ChatContextKeys.nonCopilotLanguageModelsAreUserSelectable.bindTo(_contextKeyService);
this._recentlyUsedModelIds = this._readRecentlyUsedModels();
this._pinnedModelIds = this._readPinnedModels();
this._readVisibility();
this._initChatControlData();
this._store.add(this.onDidChangeLanguageModels(() => {
let hasNonCopilotUserSelectable = false;
for (const model of this._modelCache.values()) {
}
if (model.vendor !== COPILOT_VENDOR_ID) {
break;
}
this._hasNonCopilotUserSelectableModels.set(hasNonCopilotUserSelectable);
this._refreshModelsControlManifest();
this._store.add(this._languageModelsConfigurationService.onDidChangeLanguageModelGroups(changedGroups => this._onDidChangeLanguageModelGroups(changedGroups)));
this._store.add(languageModelChatProviderExtensionPoint.setHandler((extensions, { added, removed }) => {
const addedVendors: IUserFriendlyLanguageModel[] = [];
const removedVendors: IUserFriendlyLanguageModel[] = [];
for (const extension of added) {
for (const item of Iterable.wrap(extension.value)) {
if (this._vendors.has(item.vendor)) {
extension.collector.error(localize('vscode.extension.contributes.languageModels.vendorAlreadyRegistered', "The vendor '{0}' is already registered and cannot be registered twice", item.vendor));
continue;
}
if (isFalsyOrWhitespace(item.vendor)) {
extension.collector.error(localize('vscode.extension.contributes.languageModels.emptyVendor', "The vendor field cannot be empty."));
continue;
}
if (item.vendor.trim() !== item.vendor) {
extension.collector.error(localize('vscode.extension.contributes.languageModels.whitespaceVendor', "The vendor field cannot start or end with whitespace."));
continue;
}
addedVendors.push(item);
}
}
for (const extension of removed) {
for (const item of Iterable.wrap(extension.value)) {
removedVendors.push(item);
}
}
this.deltaLanguageModelChatProviderDescriptors(addedVendors, removedVendors);
}
deltaLanguageModelChatProviderDescriptors(added: IUserFriendlyLanguageModel[], removed: IUserFriendlyLanguageModel[]): void {
const removedVendorIds: string[] = [];
for (const item of added) {
if (this._vendors.has(item.vendor)) {
this._logService.error(`The vendor '${item.vendor}' is already registered and cannot be registered twice`);
languageModels.ts ×1
continue;
}
this._logService.error('The vendor field cannot be empty.');
continue;
}
this._logService.error('The vendor field cannot start or end with whitespace.');
continue;
}
vendor: item.vendor,
displayName: item.displayName,
configuration: item.configuration,
managementCommand: item.managementCommand,
deprecation: item.deprecation,
when: item.when,
isDefault: item.vendor === COPILOT_VENDOR_ID
};
this._vendors.set(item.vendor, vendor);
addedVendorIds.push(item.vendor);
// Have some models we want from this vendor, so activate the extension
}
for (const item of removed) {
this._providers.delete(item.vendor);
this._clearModelCache(item.vendor);
this._modelsGroups.delete(item.vendor);
removedVendorIds.push(item.vendor);
}
for (const [vendor, _] of this._providers) {
this._providers.delete(vendor);
}
if (addedVendorIds.length > 0 || removedVendorIds.length > 0) {
this._onDidChangeLanguageModelVendors.fire([...addedVendorIds, ...removedVendorIds]);
if (removedVendorIds.length > 0) {
this._onLanguageModelChange.fire(vendor);
}
}
}
private async _onDidChangeLanguageModelGroups(changedGroups: readonly ILanguageModelsProviderGroup[]): Promise<void> {
const changedVendors = new Set(changedGroups.map(g => g.vendor));
await Promise.all(Array.from(changedVendors).map(vendor => this._resolveAllLanguageModels(vendor, true)));
}
getVendors(): ILanguageModelProviderDescriptor[] {
.filter(vendor => {
if (!vendor.when) {
return true; // No when clause means always visible
}
return whenClause ? this._contextKeyService.contextMatchesRules(whenClause) : false;
languageModels.ts ×2
});
}
getLanguageModelIds(): string[] {
return Array.from(this._modelCache.keys());
}
lookupLanguageModel(modelIdentifier: string): ILanguageModelChatMetadata | undefined {
}
lookupLanguageModelByQualifiedName(referenceName: string): ILanguageModelChatMetadataAndIdentifier | undefined {
for (const [identifier, model] of this._modelCache.entries()) {
if (ILanguageModelChatMetadata.matchesQualifiedName(referenceName, model)) {
return { metadata: model, identifier };
}
}
return undefined;
}
private async _resolveAllLanguageModels(vendorId: string, silent: boolean): Promise<void> {
const vendor = this._vendors.get(vendorId);
if (!vendor) {
return;
}
// If a provider is already registered (e.g. a renderer-side provider
// such as the agent host), skip the activation wait — there's nothing
// more for an extension to contribute, and waiting would block on
// extension host startup unnecessarily.
let provider = this._providers.get(vendorId);
if (!provider) {
await this._extensionService.activateByEvent(`onLanguageModelChatProvider:${vendorId}`);
provider = this._providers.get(vendorId);
}
this._logService.warn(`[LM] No provider registered for vendor ${vendorId}`);
languageModels.ts ×2
return;
}
return this._resolveLMSequencer.queue(vendorId, async () => {
const allModels: ILanguageModelChatMetadataAndIdentifier[] = [];
const languageModelsGroups: ILanguageModelsGroup[] = [];
try {
const models = await provider.provideLanguageModelChatInfo({ silent }, CancellationToken.None);
if (models.length) {
const modelIdentifiers = [];
for (const m of models) {
if (vendor.isDefault) {
// Special case for copilot models - they are all user selectable unless marked otherwise
languageModels.ts ×1
if (m.metadata.isUserSelectable !== false) {
modelIdentifiers.push(m.identifier);
} else {
this._logService.trace(`[LM] Skipping model ${m.identifier} from model picker as it is not user selectable.`);
languageModels.ts ×2
}
}
languageModelsGroups.push({ modelIdentifiers });
}
languageModelsGroups.push({
modelIdentifiers: [],
status: {
message: getErrorMessage(error),
severity: Severity.Error
}
});
}
const groups = this._languageModelsConfigurationService.getLanguageModelsProviderGroups();
const perModelConfigurations = new Map<string, IStringDictionary<unknown>>();
for (const group of groups) {
continue;
}
// For vendors without a configuration schema whose models were already
// resolved in the initial (groupless) load, groups only carry per-model
// settings and should not trigger a separate model resolution call.
// Instead, apply the per-model config to the already-resolved models.
if (!vendor.configuration && allModels.length > 0) {
if (group.settings) {
for (const model of allModels) {
const modelConfig = group.settings[model.metadata.id];
if (modelConfig) {
// Store raw config (without resolving secrets) to avoid leaking secrets on persist
perModelConfigurations.set(model.identifier, { ...modelConfig });
}
}
}
languageModelsGroups.push({ group, modelIdentifiers: [] });
continue;
}
const configuration = await this._resolveConfiguration(group, vendor.configuration);
try {
const models = await provider.provideLanguageModelChatInfo({ group: group.name, silent, configuration }, CancellationToken.None);
if (models.length) {
// Provide a sensible default for `metadata.detail` so that
// multiple instances of the same vendor (e.g. multiple
// Ollama servers) are distinguishable in the model picker.
// Providers that supply their own `detail` keep it; when
// the provider does not set one, fall back to the user-
// configured group name.
for (let i = 0; i < models.length; i++) {
if (!models[i].metadata.detail) {
models[i] = { ...models[i], metadata: { ...models[i].metadata, detail: group.name } };
languageModels.ts ×1
}
allModels.push(...models);
languageModelsGroups.push({ group, modelIdentifiers: models.map(m => m.identifier) });
}
// Collect per-model configurations from the group
if (group.settings) {
const modelConfig = group.settings[model.metadata.id];
if (modelConfig) {
// Store raw config (without resolving secrets) to avoid leaking secrets on persist
perModelConfigurations.set(model.identifier, { ...modelConfig });
}
}
}
languageModelsGroups.push({
group,
modelIdentifiers: [],
status: {
message: getErrorMessage(error),
severity: Severity.Error
}
});
}
const wasResolved = this._modelsGroups.has(vendorId);
const oldGroups = this._modelsGroups.get(vendorId) ?? [];
this._modelsGroups.set(vendorId, languageModelsGroups);
const oldModels = this._clearModelCache(vendorId);
let hasChanges = !wasResolved;
for (const model of allModels) {
this._logService.warn(`[LM] Model ${model.identifier} is already registered. Skipping.`);
continue;
}
hasChanges = hasChanges || !equals(oldModels.get(model.identifier), model.metadata);
oldModels.delete(model.identifier);
}
this._logService.trace(`[LM] Resolved language models for vendor ${vendorId}`, allModels);
languageModels.ts ×13
hasChanges = hasChanges || oldModels.size > 0;
// Also detect group structure changes (added/removed groups, status changes)
// so the UI updates even when individual models haven't changed
if (!hasChanges) {
hasChanges = this._hasGroupStructureChanged(oldGroups, languageModelsGroups);
languageModels.ts ×5
}
// Update per-model configurations for this vendor
this._clearModelConfigurations(vendorId);
for (const [identifier, config] of perModelConfigurations) {
this._modelConfigurations.set(identifier, config);
}
}
if (hasChanges) {
this._onLanguageModelChange.fire(vendorId);
} else {
this._logService.trace(`[LM] No changes in language models for vendor ${vendorId}`);
languageModels.ts ×5
}
}
private _hasGroupStructureChanged(oldGroups: readonly ILanguageModelsGroup[], newGroups: readonly ILanguageModelsGroup[]): boolean {
return true;
}
const oldGroup = oldGroups[i];
const newGroup = newGroups[i];
if (oldGroup.group?.name !== newGroup.group?.name
|| oldGroup.group?.vendor !== newGroup.group?.vendor
|| oldGroup.status?.message !== newGroup.status?.message
|| oldGroup.status?.severity !== newGroup.status?.severity
|| oldGroup.modelIdentifiers.length !== newGroup.modelIdentifiers.length) {
return true;
}
return false;
}
getLanguageModelGroups(vendor: string): ILanguageModelsGroup[] {
return this._modelsGroups.get(vendor) ?? [];
}
hasResolvedVendor(vendor: string): boolean {
return this._modelsGroups.has(vendor);
}
async selectLanguageModels(selector: ILanguageModelChatSelector): Promise<string[]> {
if (selector.vendor) {
await Promise.all(allVendors.map(vendor => this._resolveAllLanguageModels(vendor, true)));
}
const result: string[] = [];
for (const [internalModelIdentifier, model] of this._modelCache) {
&& (selector.family === undefined || model.family === selector.family)
&& (selector.version === undefined || model.version === selector.version)
&& (selector.id === undefined || model.id === selector.id)) {
result.push(internalModelIdentifier);
}
}
this._logService.trace('[LM] selected language models', selector, result);
return result;
}
registerLanguageModelProvider(vendor: string, provider: ILanguageModelChatProvider): IDisposable {
this._logService.trace('[LM] registering language model provider', vendor, provider);
languageModels.ts ×4
if (!this._vendors.has(vendor)) {
throw new Error(`Chat model provider uses UNKNOWN vendor ${vendor}.`);
}
throw new Error(`Chat model provider for vendor ${vendor} is already registered.`);
}
this._providers.set(vendor, provider);
const modelChangeListener = provider.onDidChange(() => {
return toDisposable(() => {
this._logService.trace('[LM] UNregistered language model provider', vendor);
this._clearModelCache(vendor);
this._modelsGroups.delete(vendor);
this._providers.delete(vendor);
modelChangeListener.dispose();
});
}
async sendChatRequest(modelId: string, from: ExtensionIdentifier | undefined, messages: IChatMessage[], options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise<ILanguageModelChatResponse> {
const provider = this._providers.get(metadata?.vendor || '');
if (!provider) {
throw new Error(`Chat provider for model ${modelId} is not registered.`);
}
this._logProviderUsageTelemetry(metadata);
this._maybeShowProviderDeprecationNotice(metadata);
}
const configuration = this.getModelConfiguration(modelId);
const mergedOptions = configuration ? { ...options, configuration: { ...configuration, ...options.configuration } } : options;
return provider.sendChatRequest(modelId, messages, from, mergedOptions, token);
}
/**
* When a chat request is made against a deprecated provider (one that contributes a
* `deprecation.link`), prompt the user once per session to install the replacement
* extension. The notification can be dismissed, and offers a "Don't Show Again" choice that
* is persisted across sessions via the notification service's `neverShowAgain` support.
*/
private _maybeShowProviderDeprecationNotice(metadata: ILanguageModelChatMetadata): void {
const link = vendor?.deprecation?.link;
if (!link) {
}
}
const providerName = (vendor.displayName || metadata.vendor).replace(/\s*\(deprecated\)\s*$/i, '');
languageModels.ts ×6
this._notificationService.prompt(
Severity.Info,
localize('chat.providerDeprecation.message', "The internal {0} language model provider is being deprecated. Please migrate to the official extension.", providerName),
[{
label: localize('chat.providerDeprecation.install', "Install Extension"),
run: () => { this._openerService.open(resolveProviderDeprecationLink(link, this._productService.urlProtocol)); }
}],
{
neverShowAgain: { id: `chat.providerDeprecation.${metadata.vendor}`, scope: NeverShowAgainScope.APPLICATION }
}
);
}
/**
* Reports which in-built BYOK provider (or third-party extension) backs a model request. First-party
* Copilot models are intentionally not reported here (see {@link getByokProviderTelemetryName}).
*/
private _logProviderUsageTelemetry(metadata: ILanguageModelChatMetadata | undefined): void {
const provider = getByokProviderTelemetryName(metadata?.vendor, metadata?.extension);
languageModels.ts ×6
if (!provider) {
}
provider: string;
isBYOK: boolean;
};
type LanguageModelRequestClassification = {
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.' };
isBYOK: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the model is a BYOK model.' };
owner: 'vritant24';
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.';
};
this._telemetryService.publicLog2<LanguageModelRequestEvent, LanguageModelRequestClassification>('chat.languageModelRequest', {
provider,
isBYOK: !!metadata?.isBYOK,
}
private _resolveModelConfigurationWithDefaults(modelId: string, metadata: ILanguageModelChatMetadata | undefined): IStringDictionary<unknown> | undefined {
const schema = metadata?.configurationSchema;
if (!schema?.properties && !userConfig) {
}
// Start with schema defaults
const defaults: IStringDictionary<unknown> = {};
if (propSchema.default !== undefined) {
defaults[key] = propSchema.default;
}
}
}
return undefined;
}
// User config overrides defaults
return { ...defaults, ...userConfig };
computeTokenLength(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise<number> {
const model = this._modelCache.get(modelId);
if (!model) {
throw new Error(`Chat model ${modelId} could not be found.`);
}
const provider = this._providers.get(model.vendor);
if (!provider) {
throw new Error(`Chat provider for model ${modelId} is not registered.`);
}
return provider.provideTokenCount(modelId, message, token);
}
getModelConfiguration(modelId: string): IStringDictionary<unknown> | undefined {
return this._resolveModelConfigurationWithDefaults(modelId, metadata);
}
async setModelConfiguration(modelId: string, values: IStringDictionary<unknown>): Promise<void> {
if (!metadata) {
return;
}
// Find the group from the configuration service (source of truth)
const allGroups = this._languageModelsConfigurationService.getLanguageModelsProviderGroups();
let group: ILanguageModelsProviderGroup | undefined;
// First try to find a group that already has config for this model.
group = allGroups.find(g => g.vendor === metadata.vendor && g.settings?.[metadata.id] !== undefined);
// Otherwise find the group that actually *defines* this model. Several
// groups can share the same `vendor` (e.g. multiple `customendpoint`
// providers like DeepSeek and MyCustom), so matching by vendor alone would
// write the config to the first group of that vendor — not the one the
// model belongs to. Resolve via the model→group map instead. See #322872.
if (!group) {
const vendorGroups = this._modelsGroups.get(metadata.vendor);
const containingGroup = vendorGroups?.find(vg => vg.modelIdentifiers.includes(modelId) && vg.group)?.group;
if (containingGroup) {
group = allGroups.find(g => g.vendor === containingGroup.vendor && g.name === containingGroup.name) ?? containingGroup;
}
}
// As a last resort (model not yet resolved into any group), fall back to
// any group for this vendor.
if (!group) {
group = allGroups.find(g => g.vendor === metadata.vendor);
}
// Merge new values into existing config, removing properties set to their schema default
const existingConfig = this._modelConfigurations.get(modelId) ?? {};
const updatedConfig = { ...existingConfig, ...values };
const schema = metadata.configurationSchema;
if (schema?.properties) {
for (const [key, value] of Object.entries(updatedConfig)) {
const propSchema = schema.properties[key];
if (propSchema?.default !== undefined && propSchema.default === value) {
delete updatedConfig[key];
}
}
if (group) {
const existingSettings = (group.settings as IStringDictionary<IStringDictionary<unknown>> | undefined) ?? {};
let updatedSettings: IStringDictionary<IStringDictionary<unknown>>;
if (Object.keys(updatedConfig).length === 0) {
updatedSettings = { ...existingSettings };
delete updatedSettings[metadata.id];
updatedSettings = { ...existingSettings, [metadata.id]: updatedConfig };
}
const updatedGroup: ILanguageModelsProviderGroup = {
...group,
settings: Object.keys(updatedSettings).length > 0 ? updatedSettings : undefined
};
if (!updatedGroup.settings && Object.keys(updatedGroup).filter(k => k !== 'name' && k !== 'vendor' && k !== 'range' && k !== 'modelsRange' && k !== 'settings').length === 0) {
// Remove the group entirely if it only had model config
await this._languageModelsConfigurationService.removeLanguageModelsProviderGroup(group);
await this._languageModelsConfigurationService.updateLanguageModelsProviderGroup(group, updatedGroup);
}
} else if (Object.keys(updatedConfig).length > 0) {
// Only create a new group if there's non-default config
// Use _vendors directly instead of getVendors() which filters by `when` clause,
// because we need to store config for all vendors regardless of UI visibility.
const vendor = this._vendors.get(metadata.vendor);
if (!vendor) {
return;
}
const newGroup: ILanguageModelsProviderGroup = {
name: vendor.displayName,
vendor: metadata.vendor,
settings: { [metadata.id]: updatedConfig }
};
await this._languageModelsConfigurationService.addLanguageModelsProviderGroup(newGroup);
}
// Update the in-memory cache
if (Object.keys(updatedConfig).length > 0) {
this._modelConfigurations.set(modelId, updatedConfig);
} else {
this._modelConfigurations.delete(modelId);
}
// Notify listeners so UI (e.g., model picker label) updates
this._onLanguageModelChange.fire(metadata.vendor);
}
getModelConfigurationActions(modelId: string): IAction[] {
const metadata = this._modelCache.get(modelId);
const currentConfig = this._modelConfigurations.get(modelId) ?? {};
return createModelConfigurationActions(
metadata?.configurationSchema,
currentConfig,
(key, value) => this.setModelConfiguration(modelId, { [key]: value })
);
}
async configureLanguageModelsProviderGroup(vendorId: string, providerGroupName?: string): Promise<void> {
const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId);
if (!vendor) {
throw new Error(`Vendor ${vendorId} not found.`);
}
if (vendor.managementCommand) {
await this._resolveAllLanguageModels(vendor.vendor, false);
return;
}
const languageModelProviderGroups = this._languageModelsConfigurationService.getLanguageModelsProviderGroups();
const existing = languageModelProviderGroups.find(g => g.vendor === vendorId && g.name === providerGroupName);
const name = await this.promptForName(languageModelProviderGroups, vendor, existing);
if (!name) {
return;
}
const existingConfiguration = existing ? await this._resolveConfiguration(existing, vendor.configuration) : undefined;
try {
const configuration = vendor.configuration ? await this.promptForConfiguration(name, vendor.configuration, existingConfiguration) : undefined;
if (vendor.configuration && !configuration) {
return;
}
const languageModelProviderGroup = await this._resolveLanguageModelProviderGroup(name, vendorId, configuration, vendor.configuration);
const saved = existing
? await this._languageModelsConfigurationService.updateLanguageModelsProviderGroup(existing, languageModelProviderGroup)
: await this._languageModelsConfigurationService.addLanguageModelsProviderGroup(languageModelProviderGroup);
if (vendor.configuration && this.requireConfiguring(vendor.configuration)) {
const snippet = this.getSnippetForFirstUnconfiguredProperty(configuration ?? {}, vendor.configuration);
await this._languageModelsConfigurationService.configureLanguageModels({ group: saved, snippet });
}
} catch (error) {
if (isCancellationError(error)) {
return;
}
throw error;
}
}
async renameLanguageModelsProviderGroup(vendorId: string, providerGroupName: string): Promise<void> {
const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId);
languageModels.ts ×8
if (!vendor) {
throw new Error(`Vendor ${vendorId} not found.`);
}
const languageModelProviderGroups = this._languageModelsConfigurationService.getLanguageModelsProviderGroups();
const existing = languageModelProviderGroups.find(group => group.vendor === vendorId && group.name === providerGroupName);
if (!existing) {
throw new Error(`Language model provider group ${providerGroupName} for vendor ${vendorId} not found.`);
}
const name = await this.promptForName(languageModelProviderGroups, vendor, existing);
if (!name || name === existing.name) {
return;
}
await this._languageModelsConfigurationService.updateLanguageModelsProviderGroup(existing, { ...existing, name });
}
async updateLanguageModelsProviderGroupApiKey(vendorId: string, providerGroupName: string): Promise<void> {
const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId);
languageModels.ts ×31
const schema = vendor?.configuration as IJSONSchema | undefined;
const apiKeySchema = schema?.properties?.apiKey;
if (!vendor || !schema || !apiKeySchema) {
return;
}
const existing = this._languageModelsConfigurationService.getLanguageModelsProviderGroups().find(group => group.vendor === vendorId && group.name === providerGroupName);
if (!existing) {
throw new Error(`Language model provider group ${providerGroupName} for vendor ${vendorId} not found.`);
}
try {
const existingConfiguration = await this._resolveConfiguration(existing, schema);
const apiKey = await this.promptForValue(existing.name, 'apiKey', apiKeySchema, !!schema.required?.includes('apiKey'), existingConfiguration);
if (apiKey === undefined || apiKey === existingConfiguration.apiKey) {
}
const configuration = { ...existingConfiguration, apiKey };
const updated = {
...await this._resolveLanguageModelProviderGroup(existing.name, vendorId, configuration, schema),
settings: existing.settings
};
await this._languageModelsConfigurationService.updateLanguageModelsProviderGroup(existing, updated);
await this._deleteSecretsInConfiguration(existing, schema);
} catch (error) {
if (isCancellationError(error)) {
return;
}
throw error;
}
async addLanguageModelsProviderGroupModel(vendorId: string, providerGroupName: string): Promise<void> {
const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId);
languageModels.ts ×8
const schema = vendor?.configuration as IJSONSchema | undefined;
const modelsSchema = schema?.properties?.models;
if (!vendor || !modelsSchema) {
return;
}
const group = this._languageModelsConfigurationService.getLanguageModelsProviderGroups().find(group => group.vendor === vendorId && group.name === providerGroupName);
if (!group) {
throw new Error(`Language model provider group ${providerGroupName} for vendor ${vendorId} not found.`);
}
const hasModels = Array.isArray(group.models);
const snippet = hasModels ? this.getSnippetForArrayItem(modelsSchema) : this.getSnippetForProperty('models', modelsSchema);
if (!snippet) {
return;
}
await this._languageModelsConfigurationService.configureLanguageModels({
group,
snippet,
snippetTarget: hasModels ? 'models' : 'group'
});
}
async openLanguageModelsProviderGroupSettings(vendorId: string, providerGroupName: string): Promise<void> {
const group = this._languageModelsConfigurationService.getLanguageModelsProviderGroups().find(group => group.vendor === vendorId && group.name === providerGroupName);
languageModels.ts ×2
if (!group) {
throw new Error(`Language model provider group ${providerGroupName} for vendor ${vendorId} not found.`);
}
await this._languageModelsConfigurationService.configureLanguageModels({ group });
}
async configureModel(modelId: string): Promise<void> {
const metadata = this._modelCache.get(modelId);
if (!metadata || !metadata.configurationSchema) {
return;
}
// Find the group that contains this model
const vendorGroups = this._modelsGroups.get(metadata.vendor);
let group: ILanguageModelsProviderGroup | undefined;
if (vendorGroups) {
for (const vg of vendorGroups) {
if (vg.modelIdentifiers.includes(modelId) && vg.group) {
group = vg.group;
break;
}
}
}
// If the model doesn't belong to any configured group, create one
if (!group) {
const vendor = this.getVendors().find(v => v.vendor === metadata.vendor);
if (!vendor) {
return;
}
const groupName = vendor.displayName;
const newGroup: ILanguageModelsProviderGroup = { name: groupName, vendor: metadata.vendor, settings: { [metadata.id]: {} } };
group = await this._languageModelsConfigurationService.addLanguageModelsProviderGroup(newGroup);
await this._resolveAllLanguageModels(metadata.vendor, true);
}
// Generate a snippet for the model's configuration schema
const snippet = this._getModelConfigurationSnippet(metadata.id, metadata.configurationSchema);
await this._languageModelsConfigurationService.configureLanguageModels({ group, snippet });
}
private _getModelConfigurationSnippet(modelId: string, schema: ILanguageModelConfigurationSchema): string {
const properties: string[] = [];
if (schema.properties) {
for (const [key, propSchema] of Object.entries(schema.properties)) {
if (propSchema.defaultSnippets?.[0]) {
const snippet = propSchema.defaultSnippets[0];
let bodyText = snippet.bodyText ?? JSON.stringify(snippet.body, null, '\t\t\t');
bodyText = bodyText.replace(/"(\^[^"]*)"/g, (_, value) => value.substring(1));
properties.push(`\t\t\t"${key}": ${bodyText}`);
} else if (propSchema.default !== undefined) {
properties.push(`\t\t\t"${key}": ${JSON.stringify(propSchema.default)}`);
} else {
properties.push(`\t\t\t"${key}": $\{${key}\}`);
}
}
}
const modelContent = properties.length > 0
? `{\n${properties.join(',\n')}\n\t\t}`
: '{\n\t\t\t$0\n\t\t}';
return `"settings": {\n\t\t"${modelId}": ${modelContent}\n\t}`;
}
async addLanguageModelsProviderGroup(name: string, vendorId: string, configuration: IStringDictionary<unknown> | undefined): Promise<void> {
const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId);
if (!vendor) {
throw new Error(`Vendor ${vendorId} not found.`);
}
const languageModelProviderGroup = await this._resolveLanguageModelProviderGroup(name, vendorId, configuration, vendor.configuration);
await this._languageModelsConfigurationService.addLanguageModelsProviderGroup(languageModelProviderGroup);
}
async removeLanguageModelsProviderGroup(vendorId: string, providerGroupName: string): Promise<void> {
const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId);
if (!vendor) {
throw new Error(`Vendor ${vendorId} not found.`);
}
const languageModelProviderGroups = this._languageModelsConfigurationService.getLanguageModelsProviderGroups();
const existing = languageModelProviderGroups.find(g => g.vendor === vendorId && g.name === providerGroupName);
if (!existing) {
throw new Error(`Language model provider group ${providerGroupName} for vendor ${vendorId} not found.`);
}
await this._deleteSecretsInConfiguration(existing, vendor.configuration);
await this._languageModelsConfigurationService.removeLanguageModelsProviderGroup(existing);
}
private requireConfiguring(schema: IJSONSchema): boolean {
if (schema.additionalProperties) {
return true;
}
if (!schema.properties) {
return false;
}
for (const property of Object.keys(schema.properties)) {
if (!this.canPromptForProperty(schema.properties[property])) {
return true;
}
}
return false;
}
private getSnippetForFirstUnconfiguredProperty(configuration: IStringDictionary<unknown>, schema: IJSONSchema): string | undefined {
if (!schema.properties) {
return undefined;
}
for (const property of Object.keys(schema.properties)) {
if (configuration[property] === undefined) {
const propertySchema = schema.properties[property];
const snippet = this.getSnippetForProperty(property, propertySchema);
if (snippet) {
return snippet;
}
}
}
return undefined;
}
private getSnippetForProperty(property: string, propertySchema: IJSONSchema): string | undefined {
return bodyText ? `"${property}": ${bodyText}` : undefined;
}
private getSnippetForArrayItem(propertySchema: IJSONSchema): string | undefined {
}
private getDefaultSnippetBodyText(propertySchema: IJSONSchema, arrayItem = false): string | undefined {
if (!snippet) {
return undefined;
}
const bodyText = arrayItem
? Array.isArray(snippet.body) && snippet.body.length > 0 ? JSON.stringify(snippet.body[0], null, '\t') : undefined
languageModels.ts ×2
return undefined;
}
return bodyText.replace(/"(\^[^"]*)"/g, (_, value) => value.substring(1));
}
private async promptForName(languageModelProviderGroups: readonly ILanguageModelsProviderGroup[], vendor: IUserFriendlyLanguageModel, existing: ILanguageModelsProviderGroup | undefined): Promise<string | undefined> {
if (!providerGroupName) {
providerGroupName = vendor.displayName;
let count = 1;
while (languageModelProviderGroups.some(g => g.vendor === vendor.vendor && g.name === providerGroupName)) {
count++;
providerGroupName = `${vendor.displayName} ${count}`;
}
}
let result: string | undefined;
const disposables = new DisposableStore();
try {
await new Promise<void>(resolve => {
const inputBox = disposables.add(this._quickInputService.createInputBox());
inputBox.title = localize('configureLanguageModelGroup', "Group Name");
inputBox.placeholder = localize('languageModelGroupName', "Enter a name for the group");
inputBox.value = providerGroupName;
inputBox.ignoreFocusOut = true;
disposables.add(inputBox.onDidChangeValue(value => {
if (!value) {
inputBox.validationMessage = localize('enterName', "Please enter a name");
inputBox.severity = Severity.Error;
return;
}
if (languageModelProviderGroups.some(group => group !== existing && group.vendor === vendor.vendor && group.name === value)) {
languageModels.ts ×8
inputBox.validationMessage = localize('nameExists', "A language models group with this name already exists");
inputBox.severity = Severity.Error;
return;
}
inputBox.severity = Severity.Ignore;
}));
disposables.add(inputBox.onDidAccept(async () => {
result = inputBox.value;
inputBox.hide();
}));
disposables.add(inputBox.onDidHide(() => resolve()));
inputBox.show();
});
} finally {
disposables.dispose();
}
return result;
}
private async promptForConfiguration(groupName: string, configuration: IJSONSchema, existing: IStringDictionary<unknown> | undefined): Promise<IStringDictionary<unknown> | undefined> {
if (!configuration.properties) {
return;
}
const result: IStringDictionary<unknown> = existing ? { ...existing } : {};
for (const property of Object.keys(configuration.properties)) {
const propertySchema = configuration.properties[property];
const required = !!configuration.required?.includes(property);
const value = await this.promptForValue(groupName, property, propertySchema, required, existing);
if (value !== undefined) {
result[property] = value;
}
}
return result;
}
private async promptForValue(groupName: string, property: string, propertySchema: IJSONSchema | undefined, required: boolean, existing: IStringDictionary<unknown> | undefined): Promise<unknown | undefined> {
return undefined;
}
if (!this.canPromptForProperty(propertySchema)) {
return undefined;
}
if (propertySchema.type === 'array' && propertySchema.items && !Array.isArray(propertySchema.items) && propertySchema.items.enum) {
const selectedItems = await this.promptForArray(groupName, property, propertySchema);
if (selectedItems === undefined) {
return undefined;
}
return selectedItems;
}
if (propertySchema.type === 'string' && Array.isArray(propertySchema.enum) && propertySchema.enum.length > 0) {
return this.promptForEnum(groupName, property, propertySchema, existing);
}
const value = await this.promptForInput(groupName, property, propertySchema, required, existing);
if (value === undefined) {
return undefined;
}
return value;
}
private canPromptForProperty(propertySchema: IJSONSchema | undefined): boolean {
return false;
}
if (propertySchema.type === 'array' && propertySchema.items && !Array.isArray(propertySchema.items) && propertySchema.items.enum) {
return true;
}
if (propertySchema.type === 'string' || propertySchema.type === 'number' || propertySchema.type === 'integer' || propertySchema.type === 'boolean') {
return true;
}
return false;
private getDescriptionPlaintext(propertySchema: IJSONSchema): string | undefined {
return propertySchema.description;
}
if (!md) {
return undefined;
}
// Quick input renders plain text only. Strip the inline markdown features used by
// our schemas (inline code, bold/italic, links) so users see readable help.
return md
.replace(/`([^`]+)`/g, '$1')
.replace(/\*\*([^*]+)\*\*/g, '$1')
.replace(/\*([^*]+)\*/g, '$1')
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
private async promptForArray(groupName: string, property: string, propertySchema: IJSONSchema): Promise<string[] | undefined> {
if (!propertySchema.items || Array.isArray(propertySchema.items) || !propertySchema.items.enum) {
return undefined;
}
const items = propertySchema.items.enum;
const disposables = new DisposableStore();
try {
return await new Promise<string[] | undefined>(resolve => {
const quickPick = disposables.add(this._quickInputService.createQuickPick());
quickPick.title = `${groupName}: ${propertySchema.title ?? property}`;
quickPick.items = items.map(item => ({ label: item }));
quickPick.placeholder = this.getDescriptionPlaintext(propertySchema) ?? localize('selectValue', "Select value for {0}", property);
quickPick.canSelectMany = true;
quickPick.ignoreFocusOut = true;
disposables.add(quickPick.onDidAccept(() => {
resolve(quickPick.selectedItems.map(item => item.label));
quickPick.hide();
}));
disposables.add(quickPick.onDidHide(() => {
resolve(undefined);
}));
quickPick.show();
});
} finally {
disposables.dispose();
}
}
private async promptForEnum(groupName: string, property: string, propertySchema: IJSONSchema & { enumItemLabels?: string[] }, existing: IStringDictionary<unknown> | undefined): Promise<string | undefined> {
const values = propertySchema.enum;
if (!Array.isArray(values) || values.length === 0) {
return undefined;
}
const enumDescriptions = propertySchema.enumDescriptions;
const enumItemLabels = Array.isArray(propertySchema.enumItemLabels) ? propertySchema.enumItemLabels : undefined;
const initial = existing?.[property] !== undefined ? String(existing[property]) : (propertySchema.default !== undefined ? String(propertySchema.default) : undefined);
const items: IQuickPickItem[] = values.map((value, index) => ({
label: enumItemLabels?.[index] ?? String(value),
description: enumDescriptions?.[index],
id: String(value)
}));
const disposables = new DisposableStore();
try {
return await new Promise<string | undefined>(resolve => {
const quickPick = disposables.add(this._quickInputService.createQuickPick<IQuickPickItem>());
quickPick.title = `${groupName}: ${propertySchema.title ?? property}`;
quickPick.items = items;
quickPick.placeholder = this.getDescriptionPlaintext(propertySchema) ?? localize('selectValue', "Select value for {0}", property);
quickPick.ignoreFocusOut = true;
if (initial !== undefined) {
const match = items.find(item => item.id === initial);
if (match) {
quickPick.activeItems = [match];
}
}
disposables.add(quickPick.onDidAccept(() => {
const selected = quickPick.selectedItems[0];
resolve(selected?.id);
quickPick.hide();
}));
disposables.add(quickPick.onDidHide(() => {
resolve(undefined);
}));
quickPick.show();
});
} finally {
disposables.dispose();
}
}
private async promptForInput(groupName: string, property: string, propertySchema: IJSONSchema, required: boolean, existing: IStringDictionary<unknown> | undefined): Promise<string | number | boolean | undefined> {
try {
const validate = (value: string): string | undefined => {
if (!value && required) {
return localize('valueRequired', "Value is required");
}
};
const value = await new Promise<string | undefined>((resolve, reject) => {
const inputBox = disposables.add(this._quickInputService.createInputBox());
inputBox.title = `${groupName}: ${propertySchema.title ?? property}`;
inputBox.placeholder = localize('enterValue', "Enter value for {0}", property);
inputBox.password = !!propertySchema.secret;
inputBox.ignoreFocusOut = true;
if (existing?.[property]) {
inputBox.value = String(existing?.[property]);
} else if (propertySchema.default) {
inputBox.value = String(propertySchema.default);
}
if (promptText) {
inputBox.prompt = promptText;
}
disposables.add(inputBox.onDidChangeValue(value => {
const message = validate(value);
if (message) {
inputBox.validationMessage = message;
inputBox.severity = Severity.Error;
inputBox.validationMessage = undefined;
inputBox.severity = Severity.Ignore;
}
}));
disposables.add(inputBox.onDidAccept(() => {
const message = validate(inputBox.value);
if (message) {
inputBox.validationMessage = message;
inputBox.severity = Severity.Error;
return;
}
inputBox.hide();
}));
disposables.add(inputBox.onDidHide((e) => {
if (e.reason === QuickInputHideReason.Gesture) {
reject(new CancellationError());
resolve(undefined);
}
}));
inputBox.show();
});
if (!value) {
return undefined; // User cancelled
}
if (propertySchema.type === 'number' || propertySchema.type === 'integer') {
return Number(value);
return value === 'true';
return value;
}
} finally {
disposables.dispose();
}
}
private encodeSecretKey(property: string): string {
}
private decodeSecretKey(secretInput: unknown): string | undefined {
return undefined;
}
return secretInput.substring(secretInput.indexOf(':') + 1, secretInput.length - 1);
languageModels.ts ×31
}
private _clearModelCache(vendor: string): Map<string, ILanguageModelChatMetadata> {
for (const [id, model] of this._modelCache.entries()) {
removed.set(id, model);
this._modelCache.delete(id);
}
}
}
private _clearModelConfigurations(vendor: string): void {
if (this._modelCache.get(id)?.vendor === vendor || id.startsWith(`${vendor}/`)) {
this._modelConfigurations.delete(id);
}
}
private async _resolveConfiguration(group: ILanguageModelsProviderGroup, schema: IJSONSchema | undefined): Promise<IStringDictionary<unknown>> {
}
const result: IStringDictionary<unknown> = {};
for (const key in group) {
if (key === 'vendor' || key === 'name' || key === 'range' || key === 'modelsRange' || key === 'settings') {
continue;
}
value = secretKey ? await this._secretStorageService.get(secretKey) : undefined;
}
result[key] = value;
}
return result;
private async _resolveLanguageModelProviderGroup(name: string, vendor: string, configuration: IStringDictionary<unknown> | undefined, schema: IJSONSchema | undefined): Promise<ILanguageModelsProviderGroup> {
return { name, vendor };
}
const result: IStringDictionary<unknown> = {};
for (const key in configuration) {
let value = configuration[key];
if (schema.properties?.[key]?.secret && isString(value)) {
const secretKey = `${LanguageModelsService.SECRET_KEY_PREFIX}${hash(generateUuid()).toString(16)}`;
await this._secretStorageService.set(secretKey, value);
value = this.encodeSecretKey(secretKey);
}
result[key] = value;
}
return { name, vendor, ...result };
}
private async _deleteSecretsInConfiguration(group: ILanguageModelsProviderGroup, schema: IJSONSchema | undefined): Promise<void> {
return;
}
const { vendor, name, range, modelsRange, ...configuration } = group;
for (const key in configuration) {
const value = group[key];
if (schema.properties?.[key]?.secret) {
const secretKey = this.decodeSecretKey(value);
if (secretKey) {
await this._secretStorageService.delete(secretKey);
}
}
}
}
async migrateLanguageModelsProviderGroup(languageModelsProviderGroup: ILanguageModelsProviderGroup): Promise<void> {
const { vendor, name, ...configuration } = languageModelsProviderGroup;
if (!this._vendors.get(vendor)) {
throw new Error(`Vendor ${vendor} not found.`);
}
await this._extensionService.activateByEvent(`onLanguageModelChatProvider:${vendor}`);
const provider = this._providers.get(vendor);
if (!provider) {
throw new Error(`Chat model provider for vendor ${vendor} is not registered.`);
}
await provider.provideLanguageModelChatInfo({ group: name, silent: false, configuration }, CancellationToken.None);
await this.addLanguageModelsProviderGroup(name, vendor, configuration);
}
//#region Recently used models
private _readRecentlyUsedModels(): string[] {
return this._storageService.getObject<string[]>(CHAT_MODEL_RECENTLY_USED_STORAGE_KEY, StorageScope.PROFILE, []);
languageModels.ts ×19
}
private _saveRecentlyUsedModels(): void {
this._storageService.store(CHAT_MODEL_RECENTLY_USED_STORAGE_KEY, this._recentlyUsedModelIds, StorageScope.PROFILE, StorageTarget.USER);
}
getRecentlyUsedModelIds(): string[] {
// Filter to only include models that still exist in the cache
return this._recentlyUsedModelIds
.filter(id => this._modelCache.has(id) && id !== AUTO_MODEL_IDENTIFIER)
.slice(0, 4);
}
addToRecentlyUsedList(modelIdentifier: string): void {
if (modelIdentifier === AUTO_MODEL_IDENTIFIER) {
return;
}
// Remove if already present (to move to front)
const index = this._recentlyUsedModelIds.indexOf(modelIdentifier);
if (index !== -1) {
this._recentlyUsedModelIds.splice(index, 1);
}
// Add to front
this._recentlyUsedModelIds.unshift(modelIdentifier);
// Cap at a reasonable max to avoid unbounded growth
if (this._recentlyUsedModelIds.length > 20) {
this._recentlyUsedModelIds.length = 20;
}
this._saveRecentlyUsedModels();
}
clearRecentlyUsedList(): void {
this._recentlyUsedModelIds = [];
this._saveRecentlyUsedModels();
}
//#endregion
//#region Pinned models
private _readPinnedModels(): string[] {
return this._storageService.getObject<string[]>(CHAT_MODEL_PINNED_STORAGE_KEY, StorageScope.PROFILE, []);
languageModels.ts ×19
}
private _savePinnedModels(): void {
this._storageService.store(CHAT_MODEL_PINNED_STORAGE_KEY, this._pinnedModelIds, StorageScope.PROFILE, StorageTarget.USER);
}
getPinnedModelIds(): string[] {
return this._pinnedModelIds.filter(id => id !== AUTO_MODEL_IDENTIFIER && this._modelCache.has(id));
}
pinModel(modelIdentifier: string): void {
if (modelIdentifier === AUTO_MODEL_IDENTIFIER || this._pinnedModelIds.includes(modelIdentifier)) {
return;
}
this._pinnedModelIds.push(modelIdentifier);
this._savePinnedModels();
this._onDidChangePinnedModels.fire();
}
unpinModel(modelIdentifier: string): void {
const index = this._pinnedModelIds.indexOf(modelIdentifier);
if (index === -1) {
return;
}
this._pinnedModelIds.splice(index, 1);
this._savePinnedModels();
this._onDidChangePinnedModels.fire();
}
isModelPinned(modelIdentifier: string): boolean {
return modelIdentifier !== AUTO_MODEL_IDENTIFIER && this._pinnedModelIds.includes(modelIdentifier);
}
//#endregion
//#region Model visibility
private _getGroupNameForVendor(vendor: string): string {
}
private _getModelIdsInGroup(vendor: string, groupName: string): string[] {
if (!vendorGroups) {
return [];
}
const fallbackName = this._getGroupNameForVendor(vendor);
for (const g of vendorGroups) {
const name = g.group?.name ?? fallbackName;
if (name === groupName) {
for (const id of g.modelIdentifiers) {
// Exclude agent-host BYOK copies. They are not shown as rows in this
// group (they surface under their real provider), so group-level
// visibility toggles (`isGroupHidden` / `setGroupHidden`) must not
// touch them — otherwise hiding the agent-host group would flip the
// hidden state of these copies in the underlying model set even though
// the UI never lists them here. Their visibility is owned by the real
// provider row and honoured in the picker via the reconstructed id.
const metadata = this._modelCache.get(id);
if (metadata && ILanguageModelChatMetadata.getAgentHostByokManageModelsIdentifier(metadata) !== undefined) {
}
}
}
}
return result;
}
private _readVisibility(): void {
const raw = this._storageService.getObject<{ hiddenModels?: string[] }>(CHAT_MODEL_VISIBILITY_STORAGE_KEY, StorageScope.PROFILE, {});
languageModels.ts ×19
this._hiddenModelIds = new Set(Array.isArray(raw?.hiddenModels) ? raw.hiddenModels : []);
}
private _saveVisibility(): void {
CHAT_MODEL_VISIBILITY_STORAGE_KEY,
{ hiddenModels: Array.from(this._hiddenModelIds) },
StorageScope.PROFILE,
StorageTarget.USER,
);
}
isGroupHidden(vendor: string, groupName: string): boolean {
return modelIds.length > 0 && modelIds.every(id => this._hiddenModelIds.has(id));
}
isModelHidden(modelIdentifier: string): boolean {
}
setGroupHidden(vendor: string, groupName: string, hidden: boolean): void {
const modelIds = this._getModelIdsInGroup(vendor, groupName);
for (const id of modelIds) {
if (hidden) {
if (!this._hiddenModelIds.has(id)) {
this._hiddenModelIds.add(id);
changed = true;
}
} else if (this._hiddenModelIds.delete(id)) {
}
if (changed) {
this._saveVisibility();
this._onDidChangeModelVisibility.fire();
}
}
setModelHidden(modelIdentifier: string, hidden: boolean): void {
if (hidden) {
changed = true;
}
}
this._onDidChangeModelVisibility.fire();
}
getHiddenModelIds(): string[] {
}
//#endregion
//#region Models control manifest
getModelsControlManifest(): IModelsControlManifest {
return this._modelsControlManifest;
}
private _setModelsControlManifest(response: IChatControlResponse['models']): void {
this._modelsControlRawResponse = response;
this._refreshModelsControlManifest();
}
private _refreshModelsControlManifest(): void {
const free: IStringDictionary<IModelControlEntry> = {};
const paid: IStringDictionary<IModelControlEntry> = {};
if (response?.free) {
const freeEntries = Array.isArray(response.free) ? response.free : Object.values(response.free);
for (const entry of freeEntries) {
if (!entry || !isObject(entry)) {
continue;
}
free[entry.id] = { label: entry.label, featured: entry.featured, exists: this._modelCache.has(`copilot/${entry.id}`) };
}
}
if (response?.paid) {
const paidEntries = Array.isArray(response.paid) ? response.paid : Object.values(response.paid);
for (const entry of paidEntries) {
if (!entry || !isObject(entry)) {
continue;
}
paid[entry.id] = { label: entry.label, featured: entry.featured, minVSCodeVersion: entry.minVSCodeVersion, exists: this._modelCache.has(`copilot/${entry.id}`) };
}
}
this._modelsControlManifest = { free, paid };
this._onDidChangeModelsControlManifest.fire(this._modelsControlManifest);
}
//#region Chat control data
private _initChatControlData(): void {
if (!this._chatControlUrl) {
return;
}
// Restore participant registry from storage
const raw = this._storageService.get(CHAT_PARTICIPANT_NAME_REGISTRY_STORAGE_KEY, StorageScope.APPLICATION);
try {
this._restrictedChatParticipants.set(JSON.parse(raw ?? '{}'), undefined);
languageModels.ts ×19
} catch (err) {
this._storageService.remove(CHAT_PARTICIPANT_NAME_REGISTRY_STORAGE_KEY, StorageScope.APPLICATION);
}
// Restore models control manifest from storage
const rawModels = this._storageService.get(CHAT_MODELS_CONTROL_STORAGE_KEY, StorageScope.APPLICATION);
try {
if (isObject(models)) {
this._setModelsControlManifest(models);
}
this._storageService.remove(CHAT_MODELS_CONTROL_STORAGE_KEY, StorageScope.APPLICATION);
}
this._refreshChatControlData();
private _refreshChatControlData(): void {
if (this._chatControlDisposed) {
return;
}
this._fetchChatControlData()
.catch(err => this._logService.warn('Failed to fetch chat control data', err))
.then(() => timeout(5 * 60 * 1000)) // every 5 minutes
.then(() => this._refreshChatControlData());
}
private async _fetchChatControlData(): Promise<void> {
this._logService.trace('[LM] Fetching chat control data from', this._chatControlUrl);
let context;
try {
context = await this._requestService.request({ type: 'GET', url: this._chatControlUrl!, callSite: 'languageModels.fetchChatControlData' }, CancellationToken.None);
} catch (err) {
this._logService.warn('[LM] Failed to request chat control data', getErrorMessage(err));
return;
}
if (context.res.statusCode !== 200) {
this._logService.warn(`[LM] Chat control data request failed with status ${context.res.statusCode}`);
return;
}
let result: IChatControlResponse | null;
try {
result = await asJson<IChatControlResponse>(context);
} catch (err) {
this._logService.warn('[LM] Failed to parse chat control response', getErrorMessage(err));
return;
}
this._logService.trace('[LM] Received chat control response', result ? Object.keys(result) : 'null');
if (!result || result.version !== 1) {
this._logService.warn('[LM] Unexpected chat control response version', result?.version);
return;
}
// Update restricted chat participants
const registry = result.restrictedChatParticipants;
this._restrictedChatParticipants.set(registry, undefined);
this._storageService.store(CHAT_PARTICIPANT_NAME_REGISTRY_STORAGE_KEY, JSON.stringify(registry), StorageScope.APPLICATION, StorageTarget.MACHINE);
// Update models control manifest
if (result.models) {
this._logService.trace('[LM] Updating models control manifest', { freeCount: Object.keys(result.models.free ?? {}).length, paidCount: Object.keys(result.models.paid ?? {}).length });
this._setModelsControlManifest(result.models);
this._storageService.store(CHAT_MODELS_CONTROL_STORAGE_KEY, JSON.stringify(result.models), StorageScope.APPLICATION, StorageTarget.MACHINE);
}
}
//#endregion
dispose() {
this._store.dispose();
this._providers.clear();
}
}