Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatLanguageModelActions.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';6import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';7import { IQuickInputService, IQuickPickItem, QuickPickInput } from '../../../../../platform/quickinput/common/quickInput.js';8import { ILanguageModelsService } from '../../common/languageModels.js';9import { IAuthenticationAccessService } from '../../../../services/authentication/browser/authenticationAccessService.js';10import { localize, localize2 } from '../../../../../nls.js';11import { AllowedExtension, INTERNAL_AUTH_PROVIDER_PREFIX } from '../../../../services/authentication/common/authentication.js';12import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';13import { CHAT_CATEGORY } from './chatActions.js';14import { IExtensionService } from '../../../../services/extensions/common/extensions.js';15import { IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js';16import { IProductService } from '../../../../../platform/product/common/productService.js';17import { Codicon } from '../../../../../base/common/codicons.js';18import { ThemeIcon } from '../../../../../base/common/themables.js';19import { ChatContextKeys } from '../../common/chatContextKeys.js';20import { ManageModelsAction } from './manageModelsActions.js';2122class ManageLanguageModelAuthenticationAction extends Action2 {23static readonly ID = 'workbench.action.chat.manageLanguageModelAuthentication';2425constructor() {26super({27id: ManageLanguageModelAuthenticationAction.ID,28title: localize2('manageLanguageModelAuthentication', 'Manage Language Model Access...'),29category: CHAT_CATEGORY,30precondition: ChatContextKeys.enabled,31menu: [{32id: MenuId.AccountsContext,33order: 100,34}],35f1: true36});37}3839async run(accessor: ServicesAccessor): Promise<void> {40const quickInputService = accessor.get(IQuickInputService);41const languageModelsService = accessor.get(ILanguageModelsService);42const authenticationAccessService = accessor.get(IAuthenticationAccessService);43const dialogService = accessor.get(IDialogService);44const extensionService = accessor.get(IExtensionService);45const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService);46const productService = accessor.get(IProductService);4748// Get all registered language models49const modelIds = languageModelsService.getLanguageModelIds();5051// Group models by owning extension and collect all allowed extensions52const extensionAuth = new Map<string, AllowedExtension[]>();5354const ownerToAccountLabel = new Map<string, string>();55for (const modelId of modelIds) {56const model = languageModelsService.lookupLanguageModel(modelId);57if (!model?.auth) {58continue; // Skip if model is not found59}60const ownerId = model.extension.value;61if (extensionAuth.has(ownerId)) {62// If the owner already exists, just continue63continue;64}6566// Get allowed extensions for this model's auth provider67try {68// Use providerLabel as the providerId and accountLabel (or default)69const providerId = INTERNAL_AUTH_PROVIDER_PREFIX + ownerId;70const accountLabel = model.auth.accountLabel || 'Language Models';71ownerToAccountLabel.set(ownerId, accountLabel);72const allowedExtensions = authenticationAccessService.readAllowedExtensions(73providerId,74accountLabel75).filter(ext => !ext.trusted); // Filter out trusted extensions because those should not be modified7677if (productService.trustedExtensionAuthAccess && !Array.isArray(productService.trustedExtensionAuthAccess)) {78const trustedExtensions = productService.trustedExtensionAuthAccess[providerId];79// If the provider is trusted, add all trusted extensions to the allowed list80for (const ext of trustedExtensions) {81const index = allowedExtensions.findIndex(a => a.id === ext);82if (index !== -1) {83allowedExtensions.splice(index, 1);84}85const extension = await extensionService.getExtension(ext);86if (!extension) {87continue; // Skip if the extension is not found88}89allowedExtensions.push({90id: ext,91name: extension.displayName || extension.name,92allowed: true, // Assume trusted extensions are allowed by default93trusted: true // Mark as trusted94});95}96}9798// Only grab extensions that are gettable from the extension service99const filteredExtensions = new Array<AllowedExtension>();100for (const ext of allowedExtensions) {101if (await extensionService.getExtension(ext.id)) {102filteredExtensions.push(ext);103}104}105106extensionAuth.set(ownerId, filteredExtensions);107// Add all allowed extensions to the set for this owner108} catch (error) {109// Handle error by ensuring the owner is in the map110if (!extensionAuth.has(ownerId)) {111extensionAuth.set(ownerId, []);112}113}114}115116if (extensionAuth.size === 0) {117dialogService.prompt({118type: 'info',119message: localize('noLanguageModels', 'No language models requiring authentication found.'),120detail: localize('noLanguageModelsDetail', 'There are currently no language models that require authentication.')121});122return;123}124125const items: QuickPickInput<IQuickPickItem & { extension?: AllowedExtension; ownerId?: string }>[] = [];126// Create QuickPick items grouped by owner extension127for (const [ownerId, allowedExtensions] of extensionAuth) {128const extension = await extensionService.getExtension(ownerId);129if (!extension) {130// If the extension is not found, skip it131continue;132}133// Add separator for the owning extension134items.push({135type: 'separator',136id: ownerId,137label: localize('extensionOwner', '{0}', extension.displayName || extension.name),138buttons: [{139iconClass: ThemeIcon.asClassName(Codicon.info),140tooltip: localize('openExtension', 'Open Extension'),141}]142});143144// Add allowed extensions as checkboxes (visual representation)145let addedTrustedSeparator = false;146if (allowedExtensions.length > 0) {147for (const allowedExt of allowedExtensions) {148if (allowedExt.trusted && !addedTrustedSeparator) {149items.push({150type: 'separator',151label: localize('trustedExtension', 'Trusted by Microsoft'),152});153addedTrustedSeparator = true;154}155items.push({156label: allowedExt.name,157ownerId,158id: allowedExt.id,159picked: allowedExt.allowed ?? false,160extension: allowedExt,161disabled: allowedExt.trusted, // Don't allow toggling trusted extensions162buttons: [{163iconClass: ThemeIcon.asClassName(Codicon.info),164tooltip: localize('openExtension', 'Open Extension'),165}]166});167}168} else {169items.push({170label: localize('noAllowedExtensions', 'No extensions have access'),171description: localize('noAccessDescription', 'No extensions are currently allowed to use models from {0}', ownerId),172pickable: false173});174}175}176177// Show the QuickPick178const result = await quickInputService.pick(179items,180{181canPickMany: true,182sortByLabel: true,183onDidTriggerSeparatorButton(context) {184// Handle separator button clicks185const extId = context.separator.id;186if (extId) {187// Open the extension in the editor188void extensionsWorkbenchService.open(extId);189}190},191onDidTriggerItemButton(context) {192// Handle item button clicks193const extId = context.item.id;194if (extId) {195// Open the extension in the editor196void extensionsWorkbenchService.open(extId);197}198},199title: localize('languageModelAuthTitle', 'Manage Language Model Access'),200placeHolder: localize('languageModelAuthPlaceholder', 'Choose which extensions can access language models'),201}202);203if (!result) {204return;205}206207for (const [ownerId, allowedExtensions] of extensionAuth) {208// diff with result to find out which extensions are allowed or not209// but we need to only look at the result items that have the ownerId210const allowedSet = new Set(result211.filter(item => item.ownerId === ownerId)212// only save items that are not trusted automatically213.filter(item => !item.extension?.trusted)214.map(item => item.id!));215216for (const allowedExt of allowedExtensions) {217allowedExt.allowed = allowedSet.has(allowedExt.id);218}219220authenticationAccessService.updateAllowedExtensions(221INTERNAL_AUTH_PROVIDER_PREFIX + ownerId,222ownerToAccountLabel.get(ownerId) || 'Language Models',223allowedExtensions224);225}226227}228}229230export function registerLanguageModelActions() {231registerAction2(ManageLanguageModelAuthenticationAction);232registerAction2(ManageModelsAction);233}234235236