Path: blob/main/extensions/copilot/src/extension/byok/vscode-node/abstractLanguageModelChatProvider.ts
13399 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 { CancellationToken, commands, LanguageModelChatInformation, LanguageModelChatMessage, LanguageModelChatMessage2, LanguageModelChatProvider, LanguageModelResponsePart2, PrepareLanguageModelChatModelOptions, Progress, ProvideLanguageModelChatResponseOptions } from 'vscode';6import { IConfigurationService } from '../../../platform/configuration/common/configurationService';7import { IChatModelInformation, ModelSupportedEndpoint } from '../../../platform/endpoint/common/endpointProvider';8import { ILogService } from '../../../platform/log/common/logService';9import { IFetcherService } from '../../../platform/networking/common/fetcherService';10import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';11import { IStringDictionary } from '../../../util/vs/base/common/collections';12import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';13import { CopilotLanguageModelWrapper } from '../../conversation/vscode-node/languageModelAccess';14import { BYOKAuthType, BYOKKnownModels, byokKnownModelsToAPIInfo, BYOKModelCapabilities, resolveModelInfo } from '../common/byokProvider';15import { OpenAIEndpoint } from '../node/openAIEndpoint';16import { IBYOKStorageService } from './byokStorageService';1718export interface LanguageModelChatConfiguration {19readonly apiKey?: string;20}2122export interface ExtendedLanguageModelChatInformation<C extends LanguageModelChatConfiguration> extends LanguageModelChatInformation {23readonly configuration?: C;24}2526export abstract class AbstractLanguageModelChatProvider<C extends LanguageModelChatConfiguration = LanguageModelChatConfiguration, T extends ExtendedLanguageModelChatInformation<C> = ExtendedLanguageModelChatInformation<C>> implements LanguageModelChatProvider<T> {2728constructor(29protected readonly _id: string,30protected readonly _name: string,31protected _knownModels: BYOKKnownModels | undefined,32protected readonly _byokStorageService: IBYOKStorageService,33@ILogService protected readonly _logService: ILogService,34) {35this.configureDefaultGroupWithApiKeyOnly();36}3738// TODO: Remove this after 6 months39protected async configureDefaultGroupWithApiKeyOnly(): Promise<string | undefined> {40const apiKey = await this._byokStorageService.getAPIKey(this._name);41if (apiKey) {42this.configureDefaultGroupIfExists(this._name, { apiKey } as C);43await this._byokStorageService.deleteAPIKey(this._name, BYOKAuthType.GlobalApiKey);44}45return apiKey;46}4748protected async configureDefaultGroupIfExists(name: string, configuration: C): Promise<void> {49await commands.executeCommand('lm.migrateLanguageModelsProviderGroup', { vendor: this._id, name, ...configuration });50}5152async provideLanguageModelChatInformation({ silent, configuration }: PrepareLanguageModelChatModelOptions, token: CancellationToken): Promise<T[]> {53let apiKey: string | undefined = (configuration as C)?.apiKey;54if (!apiKey) {55apiKey = await this.configureDefaultGroupWithApiKeyOnly();56}5758const models = await this.getAllModels(silent, apiKey, configuration as C);59return models.map(model => ({60...model,61apiKey,62configuration63}));64}6566abstract provideLanguageModelChatResponse(model: T, messages: Array<LanguageModelChatMessage | LanguageModelChatMessage2>, options: ProvideLanguageModelChatResponseOptions, progress: Progress<LanguageModelResponsePart2>, token: CancellationToken): Promise<void>;67abstract provideTokenCount(model: T, text: string | LanguageModelChatMessage | LanguageModelChatMessage2, token: CancellationToken): Promise<number>;68protected abstract getAllModels(silent: boolean, apiKey: string | undefined, configuration: C | undefined): Promise<T[]>;69}7071export interface OpenAICompatibleLanguageModelChatInformation<C extends LanguageModelChatConfiguration> extends ExtendedLanguageModelChatInformation<C> {72url: string;73}7475export abstract class AbstractOpenAICompatibleLMProvider<T extends LanguageModelChatConfiguration = LanguageModelChatConfiguration> extends AbstractLanguageModelChatProvider<T, OpenAICompatibleLanguageModelChatInformation<T>> {76protected readonly _lmWrapper: CopilotLanguageModelWrapper;7778constructor(79id: string,80name: string,81knownModels: BYOKKnownModels | undefined,82byokStorageService: IBYOKStorageService,83@IFetcherService protected readonly _fetcherService: IFetcherService,84logService: ILogService,85@IInstantiationService protected readonly _instantiationService: IInstantiationService,86@IConfigurationService protected readonly _configurationService: IConfigurationService,87@IExperimentationService protected readonly _expService: IExperimentationService88) {89super(id, name, knownModels, byokStorageService, logService);90this._lmWrapper = this._instantiationService.createInstance(CopilotLanguageModelWrapper);91}9293async provideLanguageModelChatResponse(model: OpenAICompatibleLanguageModelChatInformation<T>, messages: Array<LanguageModelChatMessage | LanguageModelChatMessage2>, options: ProvideLanguageModelChatResponseOptions, progress: Progress<LanguageModelResponsePart2>, token: CancellationToken): Promise<void> {94const openAIChatEndpoint = await this.createOpenAIEndPoint(model);95return this._lmWrapper.provideLanguageModelResponse(openAIChatEndpoint, messages, options, options.requestInitiator, progress, token);96}9798async provideTokenCount(model: OpenAICompatibleLanguageModelChatInformation<T>, text: string | LanguageModelChatMessage | LanguageModelChatMessage2, token: CancellationToken): Promise<number> {99const openAIChatEndpoint = await this.createOpenAIEndPoint(model);100return this._lmWrapper.provideTokenCount(openAIChatEndpoint, text);101}102103protected async getAllModels(silent: boolean, apiKey: string | undefined, configuration: T | undefined): Promise<OpenAICompatibleLanguageModelChatInformation<T>[]> {104const modelsUrl = this.getModelsBaseUrl(configuration);105if (modelsUrl) {106const models = await this.getModelsFromEndpoint(modelsUrl, silent, apiKey);107return byokKnownModelsToAPIInfo(this._name, models).map(model => ({108...model,109url: modelsUrl110}));111}112return [];113}114115private async getModelsFromEndpoint(endpoint: string, silent: boolean, apiKey: string | undefined): Promise<BYOKKnownModels> {116if (!apiKey && silent) {117return {};118}119120try {121const headers: IStringDictionary<string> = {122'Content-Type': 'application/json',123'Authorization': `Bearer ${apiKey}`124};125126const modelsEndpoint = this.getModelsDiscoveryUrl(endpoint);127const response = await this._fetcherService.fetch(modelsEndpoint, {128method: 'GET',129headers,130callSite: 'byok-models-discovery',131});132const data = await response.json();133const modelList: BYOKKnownModels = {};134135const models = data.data ?? data.models;136if (!models || !Array.isArray(models)) {137throw new Error('Invalid response format');138}139140for (const model of models) {141let modelCapabilities = this._knownModels?.[model.id];142if (!modelCapabilities) {143modelCapabilities = this.resolveModelCapabilities(model);144if (!modelCapabilities) {145continue;146}147if (!this._knownModels) {148this._knownModels = {};149}150this._knownModels[model.id] = modelCapabilities;151}152modelList[model.id] = modelCapabilities;153}154return modelList;155} catch (error) {156this._logService.error(error, `Error fetching available OpenRouter models`);157throw error;158}159}160161protected async createOpenAIEndPoint(model: OpenAICompatibleLanguageModelChatInformation<T>): Promise<OpenAIEndpoint> {162const modelInfo = this.getModelInfo(model.id, model.url);163const url = modelInfo.supported_endpoints?.includes(ModelSupportedEndpoint.Responses) ?164`${model.url}/responses` :165`${model.url}/chat/completions`;166return this._instantiationService.createInstance(OpenAIEndpoint, modelInfo, model.configuration?.apiKey ?? '', url);167}168169protected getModelInfo(modelId: string, modelUrl: string): IChatModelInformation {170return resolveModelInfo(modelId, this._name, this._knownModels);171}172173protected resolveModelCapabilities(modelData: unknown): BYOKModelCapabilities | undefined {174return undefined;175}176177protected abstract getModelsBaseUrl(configuration: T | undefined): string | undefined;178179protected getModelsDiscoveryUrl(modelsBaseUrl: string): string {180return `${modelsBaseUrl}/models`;181}182183}184185