Path: blob/main/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts
4780 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 { VSBuffer } from '../../../../base/common/buffer.js';6import { Emitter, Event } from '../../../../base/common/event.js';7import { Disposable } from '../../../../base/common/lifecycle.js';8import { Mutable } from '../../../../base/common/types.js';9import { URI } from '../../../../base/common/uri.js';10import { IFileService } from '../../../../platform/files/common/files.js';11import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';12import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';13import { ITextEditorService } from '../../../services/textfile/common/textEditorService.js';14import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';15import { equals } from '../../../../base/common/objects.js';16import { IRange } from '../../../../editor/common/core/range.js';17import { JSONVisitor, visit } from '../../../../base/common/json.js';18import { ITextModel } from '../../../../editor/common/model.js';19import { ITextModelService } from '../../../../editor/common/services/resolverService.js';20import { ITextFileService } from '../../../services/textfile/common/textfiles.js';21import { getCodeEditor } from '../../../../editor/browser/editorBrowser.js';22import { ILanguageModelsConfigurationService, ILanguageModelsProviderGroup } from '../common/languageModelsConfiguration.js';23import { IJSONContributionRegistry, Extensions as JSONExtensions } from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js';24import { Registry } from '../../../../platform/registry/common/platform.js';25import { IWorkbenchContribution } from '../../../common/contributions.js';26import { ILanguageModelsService } from '../common/languageModels.js';27import { IJSONSchema } from '../../../../base/common/jsonSchema.js';2829type LanguageModelsProviderGroups = Mutable<ILanguageModelsProviderGroup>[];3031export class LanguageModelsConfigurationService extends Disposable implements ILanguageModelsConfigurationService {3233declare _serviceBrand: undefined;3435private readonly modelsConfigurationFile: URI;3637private readonly _onDidChangeLanguageModelGroups = new Emitter<void>();38readonly onDidChangeLanguageModelGroups: Event<void> = this._onDidChangeLanguageModelGroups.event;3940private languageModelsProviderGroups: LanguageModelsProviderGroups = [];4142constructor(43@IFileService private readonly fileService: IFileService,44@ITextFileService private readonly textFileService: ITextFileService,45@ITextModelService private readonly textModelService: ITextModelService,46@IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService,47@ITextEditorService private readonly textEditorService: ITextEditorService,48@IUserDataProfileService userDataProfileService: IUserDataProfileService,49@IUriIdentityService uriIdentityService: IUriIdentityService,50) {51super();52this.modelsConfigurationFile = uriIdentityService.extUri.joinPath(userDataProfileService.currentProfile.location, 'models.json');53this.updateLanguageModelsConfiguration();54this._register(fileService.watch(this.modelsConfigurationFile));55this._register(fileService.onDidFilesChange(e => {56if (e.contains(this.modelsConfigurationFile)) {57this.updateLanguageModelsConfiguration();58}59}));60}6162private setLanguageModelsConfiguration(languageModelsConfiguration: LanguageModelsProviderGroups): void {63if (equals(this.languageModelsProviderGroups, languageModelsConfiguration)) {64return;65}66this.languageModelsProviderGroups = languageModelsConfiguration;67this._onDidChangeLanguageModelGroups.fire();68}6970private async updateLanguageModelsConfiguration(): Promise<void> {71const languageModelsProviderGroups = await this.withLanguageModelsProviderGroups();72this.setLanguageModelsConfiguration(languageModelsProviderGroups);73}7475getLanguageModelsProviderGroups(): readonly ILanguageModelsProviderGroup[] {76return this.languageModelsProviderGroups;77}7879async addLanguageModelsProviderGroup(toAdd: ILanguageModelsProviderGroup): Promise<ILanguageModelsProviderGroup> {80await this.withLanguageModelsProviderGroups(async languageModelsProviderGroups => {81if (languageModelsProviderGroups.some(({ name, vendor }) => name === toAdd.name && vendor === toAdd.vendor)) {82throw new Error(`Language model group with name ${toAdd.name} already exists for vendor ${toAdd.vendor}`);83}84languageModelsProviderGroups.push(toAdd);85return languageModelsProviderGroups;86});8788await this.updateLanguageModelsConfiguration();89const result = this.getLanguageModelsProviderGroups().find(group => group.name === toAdd.name && group.vendor === toAdd.vendor);90if (!result) {91throw new Error(`Language model group with name ${toAdd.name} not found for vendor ${toAdd.vendor}`);92}93return result;94}9596async updateLanguageModelsProviderGroup(toUpdate: ILanguageModelsProviderGroup): Promise<ILanguageModelsProviderGroup> {97await this.withLanguageModelsProviderGroups(async languageModelsProviderGroups => {98const result: LanguageModelsProviderGroups = [];99for (const group of languageModelsProviderGroups) {100if (group.name === toUpdate.name && group.vendor === toUpdate.vendor) {101result.push(toUpdate);102} else {103result.push(group);104}105}106return result;107});108109await this.updateLanguageModelsConfiguration();110const result = this.getLanguageModelsProviderGroups().find(group => group.name === toUpdate.name && group.vendor === toUpdate.vendor);111if (!result) {112throw new Error(`Language model group with name ${toUpdate.name} not found for vendor ${toUpdate.vendor}`);113}114return result;115}116117async removeLanguageModelsProviderGroup(toRemove: ILanguageModelsProviderGroup): Promise<void> {118await this.withLanguageModelsProviderGroups(async languageModelsProviderGroups => {119const result: LanguageModelsProviderGroups = [];120for (const group of languageModelsProviderGroups) {121if (group.name === toRemove.name && group.vendor === toRemove.vendor) {122continue;123}124result.push(group);125}126return result;127});128await this.updateLanguageModelsConfiguration();129}130131async configureLanguageModels(range?: IRange): Promise<void> {132const editor = await this.editorGroupsService.activeGroup.openEditor(this.textEditorService.createTextEditor({ resource: this.modelsConfigurationFile }));133if (!editor || !range) {134return;135}136137const codeEditor = getCodeEditor(editor.getControl());138if (!codeEditor) {139return;140}141142const position = { lineNumber: range.startLineNumber, column: range.startColumn };143codeEditor.setPosition(position);144codeEditor.revealPositionNearTop(position);145codeEditor.focus();146}147148private async withLanguageModelsProviderGroups(update?: (languageModelsProviderGroups: LanguageModelsProviderGroups) => Promise<LanguageModelsProviderGroups>): Promise<LanguageModelsProviderGroups> {149const exists = await this.fileService.exists(this.modelsConfigurationFile);150if (!exists) {151await this.fileService.writeFile(this.modelsConfigurationFile, VSBuffer.fromString(JSON.stringify([], undefined, '\t')));152}153const ref = await this.textModelService.createModelReference(this.modelsConfigurationFile);154const model = ref.object.textEditorModel;155try {156const languageModelsProviderGroups = parseLanguageModelsProviderGroups(model);157if (!update) {158return languageModelsProviderGroups;159}160const updatedLanguageModelsProviderGroups = await update(languageModelsProviderGroups);161for (const group of updatedLanguageModelsProviderGroups) {162delete group.range;163}164model.setValue(JSON.stringify(updatedLanguageModelsProviderGroups, undefined, '\t'));165await this.textFileService.save(this.modelsConfigurationFile);166return updatedLanguageModelsProviderGroups;167} finally {168ref.dispose();169}170}171}172173export function parseLanguageModelsProviderGroups(model: ITextModel): LanguageModelsProviderGroups {174const configuration: LanguageModelsProviderGroups = [];175let currentProperty: string | null = null;176let currentParent: unknown = configuration;177const previousParents: unknown[] = [];178179function onValue(value: unknown, offset: number, length: number) {180if (Array.isArray(currentParent)) {181(currentParent as unknown[]).push(value);182} else if (currentProperty !== null) {183(currentParent as Record<string, unknown>)[currentProperty] = value;184if (currentProperty === 'configuration') {185const start = model.getPositionAt(offset);186const range: Mutable<IRange> = {187startLineNumber: start.lineNumber,188startColumn: start.column,189endLineNumber: start.lineNumber,190endColumn: start.column191};192if (value && typeof value === 'object') {193(value as { _parentConfigurationRange?: Mutable<IRange> })._parentConfigurationRange = range;194} else {195const end = model.getPositionAt(offset + length);196range.endLineNumber = end.lineNumber;197range.endColumn = end.column;198}199(currentParent as { configurationRange?: IRange }).configurationRange = range;200}201}202}203204const visitor: JSONVisitor = {205onObjectBegin: (offset: number, length: number) => {206const object: Record<string, unknown> & { range?: IRange } = {};207if (Array.isArray(currentParent)) {208const start = model.getPositionAt(offset);209const end = model.getPositionAt(offset + length);210object.range = {211startLineNumber: start.lineNumber,212startColumn: start.column,213endLineNumber: end.lineNumber,214endColumn: end.column215};216}217onValue(object, offset, length);218previousParents.push(currentParent);219currentParent = object;220currentProperty = null;221},222onObjectProperty: (name: string, offset: number, length: number) => {223currentProperty = name;224},225onObjectEnd: (offset: number, length: number) => {226const parent = currentParent as Record<string, unknown> & { range?: IRange; _parentConfigurationRange?: Mutable<IRange> };227if (parent.range) {228const end = model.getPositionAt(offset + length);229parent.range = {230startLineNumber: parent.range.startLineNumber,231startColumn: parent.range.startColumn,232endLineNumber: end.lineNumber,233endColumn: end.column234};235}236if (parent._parentConfigurationRange) {237const end = model.getPositionAt(offset + length);238parent._parentConfigurationRange.endLineNumber = end.lineNumber;239parent._parentConfigurationRange.endColumn = end.column;240delete parent._parentConfigurationRange;241}242currentParent = previousParents.pop();243},244onArrayBegin: (offset: number, length: number) => {245if (currentParent === configuration && previousParents.length === 0) {246previousParents.push(currentParent);247currentProperty = null;248return;249}250const array: unknown[] = [];251onValue(array, offset, length);252previousParents.push(currentParent);253currentParent = array;254currentProperty = null;255},256onArrayEnd: (offset: number, length: number) => {257const parent = currentParent as { _parentConfigurationRange?: Mutable<IRange> };258if (parent._parentConfigurationRange) {259const end = model.getPositionAt(offset + length);260parent._parentConfigurationRange.endLineNumber = end.lineNumber;261parent._parentConfigurationRange.endColumn = end.column;262delete parent._parentConfigurationRange;263}264currentParent = previousParents.pop();265},266onLiteralValue: (value: unknown, offset: number, length: number) => {267onValue(value, offset, length);268},269};270visit(model.getValue(), visitor);271return configuration;272}273274const languageModelsSchemaId = 'vscode://schemas/language-models';275276export class ChatLanguageModelsDataContribution extends Disposable implements IWorkbenchContribution {277278static readonly ID = 'workbench.contrib.chatLanguageModelsData';279280constructor(281@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,282@IUserDataProfileService userDataProfileService: IUserDataProfileService,283@IUriIdentityService uriIdentityService: IUriIdentityService,284) {285super();286const modelsConfigurationFile = uriIdentityService.extUri.joinPath(userDataProfileService.currentProfile.location, 'models.json');287const registry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);288this._register(registry.registerSchemaAssociation(languageModelsSchemaId, modelsConfigurationFile.toString()));289290this.updateSchema(registry);291this._register(this.languageModelsService.onDidChangeLanguageModels(() => this.updateSchema(registry)));292}293294private updateSchema(registry: IJSONContributionRegistry): void {295const vendors = this.languageModelsService.getVendors();296297const schema: IJSONSchema = {298type: 'array',299items: {300properties: {301vendor: {302type: 'string',303enum: vendors.map(v => v.vendor)304},305name: { type: 'string' }306},307allOf: vendors.map(vendor => ({308if: {309properties: {310vendor: { const: vendor.vendor }311}312},313then: vendor.configuration314})),315required: ['vendor', 'name']316}317};318319registry.registerSchema(languageModelsSchemaId, schema);320}321}322323324