Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/byok/vscode-node/customOAIProvider.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Config, ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
7
import { EndpointEditToolName, ModelSupportedEndpoint } from '../../../platform/endpoint/common/endpointProvider';
8
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
9
import { ILogService } from '../../../platform/log/common/logService';
10
import { IFetcherService } from '../../../platform/networking/common/fetcherService';
11
import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';
12
import { IStringDictionary } from '../../../util/vs/base/common/collections';
13
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
14
import { byokKnownModelToAPIInfo, resolveModelInfo } from '../common/byokProvider';
15
import { OpenAIEndpoint } from '../node/openAIEndpoint';
16
import { AbstractOpenAICompatibleLMProvider, LanguageModelChatConfiguration, OpenAICompatibleLanguageModelChatInformation } from './abstractLanguageModelChatProvider';
17
import { IBYOKStorageService } from './byokStorageService';
18
19
export function resolveCustomOAIUrl(modelId: string, url: string): string {
20
// The fully resolved url was already passed in
21
if (hasExplicitApiPath(url)) {
22
return url;
23
}
24
25
// Remove the trailing slash
26
if (url.endsWith('/')) {
27
url = url.slice(0, -1);
28
}
29
30
// Default to chat completions for base URLs
31
const defaultApiPath = '/chat/completions';
32
33
// Check if URL already contains any version pattern like /v1, /v2, etc
34
const versionPattern = /\/v\d+$/;
35
if (versionPattern.test(url)) {
36
return `${url}${defaultApiPath}`;
37
}
38
39
// For standard OpenAI-compatible endpoints, just append the standard path
40
return `${url}/v1${defaultApiPath}`;
41
}
42
43
export function hasExplicitApiPath(url: string): boolean {
44
return url.includes('/responses') || url.includes('/chat/completions');
45
}
46
47
export interface CustomOAIModelProviderConfig extends LanguageModelChatConfiguration {
48
url?: string;
49
models?: CustomOAIModelConfig[];
50
}
51
52
interface _CustomOAIModelConfig {
53
name: string;
54
url: string;
55
maxInputTokens: number;
56
maxOutputTokens: number;
57
toolCalling: boolean;
58
vision: boolean;
59
thinking?: boolean;
60
streaming?: boolean;
61
editTools?: EndpointEditToolName[];
62
requestHeaders?: Record<string, string>;
63
zeroDataRetentionEnabled?: boolean;
64
}
65
66
export interface CustomOAIModelConfig extends _CustomOAIModelConfig {
67
id: string;
68
}
69
70
export abstract class AbstractCustomOAIBYOKModelProvider extends AbstractOpenAICompatibleLMProvider<CustomOAIModelProviderConfig> {
71
72
constructor(
73
id: string,
74
name: string,
75
byokStorageService: IBYOKStorageService,
76
@ILogService logService: ILogService,
77
@IFetcherService fetcherService: IFetcherService,
78
@IInstantiationService instantiationService: IInstantiationService,
79
@IConfigurationService configurationService: IConfigurationService,
80
@IExperimentationService expService: IExperimentationService,
81
@IVSCodeExtensionContext private readonly _extensionContext: IVSCodeExtensionContext
82
) {
83
super(id, name, undefined, byokStorageService, fetcherService, logService, instantiationService, configurationService, expService);
84
}
85
86
protected async migrateConfig(configKey: Config<IStringDictionary<_CustomOAIModelConfig>>, providerName: string, providerGroupName: string): Promise<void> {
87
// Check if migration has already been completed
88
const migrationKey = `copilot-byok-migration-${providerName}-${configKey}`;
89
const migrationCompleted = this._extensionContext.globalState.get<boolean>(migrationKey, false);
90
if (migrationCompleted) {
91
return;
92
}
93
94
const customOAIModelConfigsByApiKey: Map<string, Array<CustomOAIModelConfig & { requiresAPIKey?: boolean }>> = new Map();
95
const customOAIModelProviderConfig = this._configurationService.getConfig<IStringDictionary<_CustomOAIModelConfig>>(configKey);
96
for (const [modelId, modelConfig] of Object.entries(customOAIModelProviderConfig)) {
97
const apiKey = await this._byokStorageService.getAPIKey(providerName, modelId) ?? '';
98
const customOAIModelConfigs = customOAIModelConfigsByApiKey.get(apiKey) ?? [];
99
customOAIModelConfigs.push({ ...modelConfig, id: modelId, requiresAPIKey: undefined });
100
customOAIModelConfigsByApiKey.set(apiKey, customOAIModelConfigs);
101
}
102
if (customOAIModelConfigsByApiKey.size > 0) {
103
for (const [apiKey, customOAIModelConfigs] of customOAIModelConfigsByApiKey.entries()) {
104
await this.configureDefaultGroupIfExists(providerGroupName, { models: customOAIModelConfigs, apiKey: apiKey || undefined });
105
}
106
// Mark migration as completed instead of deleting the config
107
await this._extensionContext.globalState.update(migrationKey, true);
108
}
109
}
110
111
protected override async configureDefaultGroupWithApiKeyOnly(): Promise<string | undefined> {
112
// No-op: Custom OAI models are configured separately via migration
113
return;
114
}
115
116
protected override async getAllModels(silent: boolean, apiKey: string | undefined, configuration: CustomOAIModelProviderConfig | undefined): Promise<OpenAICompatibleLanguageModelChatInformation<CustomOAIModelProviderConfig>[]> {
117
if (configuration?.url) {
118
return super.getAllModels(silent, apiKey, configuration);
119
}
120
const models: OpenAICompatibleLanguageModelChatInformation<CustomOAIModelProviderConfig>[] = [];
121
if (Array.isArray(configuration?.models)) {
122
for (const modelConfig of configuration.models) {
123
models.push({
124
...byokKnownModelToAPIInfo(this._name, modelConfig.id, modelConfig),
125
url: modelConfig.url
126
});
127
}
128
}
129
return models;
130
}
131
132
protected override async createOpenAIEndPoint(model: OpenAICompatibleLanguageModelChatInformation<CustomOAIModelProviderConfig>): Promise<OpenAIEndpoint> {
133
const url = this.resolveUrl(model.id, model.url);
134
const modelConfiguration = model.configuration?.models?.find(m => m.id === model.id);
135
const modelCapabilities = {
136
maxInputTokens: model.maxInputTokens,
137
maxOutputTokens: model.maxOutputTokens,
138
toolCalling: !!model.capabilities?.toolCalling || false,
139
vision: !!model.capabilities?.imageInput || false,
140
name: model.name,
141
url,
142
thinking: modelConfiguration?.thinking ?? false,
143
streaming: modelConfiguration?.streaming,
144
requestHeaders: modelConfiguration?.requestHeaders,
145
zeroDataRetentionEnabled: modelConfiguration?.zeroDataRetentionEnabled
146
};
147
const modelInfo = resolveModelInfo(model.id, this._name, undefined, modelCapabilities);
148
if (modelCapabilities?.url?.includes('/responses')) {
149
modelInfo.supported_endpoints = [
150
ModelSupportedEndpoint.ChatCompletions,
151
ModelSupportedEndpoint.Responses
152
];
153
}
154
return this._instantiationService.createInstance(OpenAIEndpoint, modelInfo, model.configuration?.apiKey ?? '', url);
155
}
156
157
protected getModelsBaseUrl(configuration: CustomOAIModelProviderConfig | undefined): string | undefined {
158
return configuration?.url;
159
}
160
161
protected abstract resolveUrl(modelId: string, url: string): string;
162
}
163
164
export class CustomOAIBYOKModelProvider extends AbstractCustomOAIBYOKModelProvider {
165
166
static readonly providerName: string = 'CustomOAI';
167
private providerName: string = CustomOAIBYOKModelProvider.providerName;
168
169
constructor(
170
_byokStorageService: IBYOKStorageService,
171
@ILogService logService: ILogService,
172
@IFetcherService fetcherService: IFetcherService,
173
@IInstantiationService instantiationService: IInstantiationService,
174
@IConfigurationService configurationService: IConfigurationService,
175
@IExperimentationService expService: IExperimentationService,
176
@IVSCodeExtensionContext extensionContext: IVSCodeExtensionContext
177
) {
178
super(CustomOAIBYOKModelProvider.providerName.toLowerCase(), CustomOAIBYOKModelProvider.providerName, _byokStorageService, logService, fetcherService, instantiationService, configurationService, expService, extensionContext);
179
this.migrateExistingConfigs();
180
}
181
182
// TODO: Remove this after 6 months
183
private async migrateExistingConfigs(): Promise<void> {
184
await this.migrateConfig(ConfigKey.Deprecated.CustomOAIModels, this.providerName, this.providerName);
185
}
186
187
protected resolveUrl(modelId: string, url: string): string {
188
return resolveCustomOAIUrl(modelId, url);
189
}
190
}
191