Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/byok/common/byokProvider.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
import type { Disposable, LanguageModelChatInformation, LanguageModelDataPart, LanguageModelTextPart, LanguageModelThinkingPart, LanguageModelToolCallPart, LanguageModelToolResultPart } from 'vscode';
6
import { CopilotToken } from '../../../platform/authentication/common/copilotToken';
7
import { ICAPIClientService } from '../../../platform/endpoint/common/capiClient';
8
import { EndpointEditToolName, IChatModelInformation, ModelSupportedEndpoint } from '../../../platform/endpoint/common/endpointProvider';
9
import { isScenarioAutomation } from '../../../platform/env/common/envService';
10
import { TokenizerType } from '../../../util/common/tokenizer';
11
12
export const enum BYOKAuthType {
13
/**
14
* Requires a single API key for all models (e.g., OpenAI)
15
*/
16
GlobalApiKey,
17
/**
18
* Requires both deployment URL and API key per model (e.g., Azure)
19
*/
20
PerModelDeployment,
21
/**
22
* No authentication required (e.g., Ollama)
23
*/
24
None
25
}
26
27
interface BYOKBaseModelConfig {
28
modelId: string;
29
capabilities?: BYOKModelCapabilities;
30
}
31
32
export type LMResponsePart = LanguageModelTextPart | LanguageModelToolCallPart | LanguageModelDataPart | LanguageModelThinkingPart | LanguageModelToolResultPart;
33
34
export interface BYOKGlobalKeyModelConfig extends BYOKBaseModelConfig {
35
apiKey: string;
36
}
37
38
export interface BYOKPerModelConfig extends BYOKBaseModelConfig {
39
apiKey: string;
40
deploymentUrl: string;
41
}
42
43
interface BYOKNoAuthModelConfig extends BYOKBaseModelConfig {
44
// No additional fields required
45
}
46
47
export type BYOKModelConfig = BYOKGlobalKeyModelConfig | BYOKPerModelConfig | BYOKNoAuthModelConfig;
48
49
export interface BYOKModelCapabilities {
50
name: string;
51
url?: string;
52
maxInputTokens: number;
53
maxOutputTokens: number;
54
toolCalling: boolean;
55
vision: boolean;
56
thinking?: boolean;
57
adaptiveThinking?: boolean;
58
streaming?: boolean;
59
editTools?: EndpointEditToolName[];
60
requestHeaders?: Record<string, string>;
61
supportedEndpoints?: ModelSupportedEndpoint[];
62
zeroDataRetentionEnabled?: boolean;
63
supportsReasoningEffort?: string[];
64
}
65
66
export interface BYOKModelRegistry {
67
readonly name: string;
68
readonly authType: BYOKAuthType;
69
updateKnownModelsList(knownModels: BYOKKnownModels | undefined): void;
70
getAllModels(apiKey?: string): Promise<{ id: string; name: string }[]>;
71
registerModel(config: BYOKModelConfig): Promise<Disposable>;
72
}
73
74
// Many model providers don't have robust model lists. This allows us to map id -> information about models, and then if we don't know the model just let the user enter a custom id
75
export type BYOKKnownModels = Record<string, BYOKModelCapabilities>;
76
77
// Type guards to ensure correct config type
78
export function isGlobalKeyConfig(config: BYOKModelConfig): config is BYOKGlobalKeyModelConfig {
79
return 'apiKey' in config && !('deploymentUrl' in config);
80
}
81
82
export function isPerModelConfig(config: BYOKModelConfig): config is BYOKPerModelConfig {
83
return 'apiKey' in config && 'deploymentUrl' in config;
84
}
85
86
export function isNoAuthConfig(config: BYOKModelConfig): config is BYOKNoAuthModelConfig {
87
return !('apiKey' in config) && !('deploymentUrl' in config);
88
}
89
90
export function resolveModelInfo(modelId: string, providerName: string, knownModels: BYOKKnownModels | undefined, modelCapabilities?: BYOKModelCapabilities): IChatModelInformation {
91
// Model Capabilities are something the user has decided on so those take precedence, then we rely on known model info, then defaults.
92
let knownModelInfo = modelCapabilities;
93
if (knownModels && !knownModelInfo) {
94
knownModelInfo = knownModels[modelId];
95
}
96
const modelName = knownModelInfo?.name || modelId;
97
const contextWinow = knownModelInfo ? (knownModelInfo.maxInputTokens + knownModelInfo.maxOutputTokens) : 128000;
98
const modelInfo: IChatModelInformation = {
99
id: modelId,
100
name: modelName,
101
vendor: providerName,
102
version: '1.0.0',
103
capabilities: {
104
type: 'chat',
105
family: modelId,
106
supports: {
107
streaming: knownModelInfo?.streaming ?? true,
108
tool_calls: !!knownModelInfo?.toolCalling,
109
vision: !!knownModelInfo?.vision,
110
thinking: !!knownModelInfo?.thinking,
111
adaptive_thinking: !!knownModelInfo?.adaptiveThinking
112
},
113
tokenizer: TokenizerType.O200K,
114
limits: {
115
max_context_window_tokens: contextWinow,
116
max_prompt_tokens: knownModelInfo?.maxInputTokens || 100000,
117
max_output_tokens: knownModelInfo?.maxOutputTokens || 8192
118
}
119
},
120
is_chat_default: false,
121
is_chat_fallback: false,
122
model_picker_enabled: true,
123
supported_endpoints: knownModelInfo?.supportedEndpoints,
124
zeroDataRetentionEnabled: knownModelInfo?.zeroDataRetentionEnabled
125
};
126
if (knownModelInfo?.requestHeaders && Object.keys(knownModelInfo.requestHeaders).length > 0) {
127
modelInfo.requestHeaders = { ...knownModelInfo.requestHeaders };
128
}
129
return modelInfo;
130
}
131
132
export function byokKnownModelsToAPIInfo(providerName: string, knownModels: BYOKKnownModels | undefined): LanguageModelChatInformation[] {
133
if (!knownModels) {
134
return [];
135
}
136
return Object.entries(knownModels).map(([id, capabilities]) => byokKnownModelToAPIInfo(providerName, id, capabilities));
137
}
138
139
export function byokKnownModelToAPIInfo(providerName: string, id: string, capabilities: BYOKModelCapabilities): LanguageModelChatInformation {
140
return {
141
id,
142
name: capabilities.name,
143
version: '1.0.0',
144
maxOutputTokens: capabilities.maxOutputTokens,
145
maxInputTokens: capabilities.maxInputTokens,
146
// `detail` is intentionally omitted: when this model is resolved
147
// via a configured provider group, `LanguageModelsService` will
148
// fall back to the group name so multiple instances of the same
149
// vendor (e.g. multiple Ollama servers) are distinguishable in
150
// the model picker.
151
family: id,
152
tooltip: `${capabilities.name} is contributed via the ${providerName} provider.`,
153
multiplierNumeric: 0,
154
isUserSelectable: true,
155
capabilities: {
156
toolCalling: capabilities.toolCalling,
157
imageInput: capabilities.vision
158
},
159
};
160
}
161
162
export function isBYOKEnabled(copilotToken: Omit<CopilotToken, 'token'>, capiClientService: ICAPIClientService): boolean {
163
if (isScenarioAutomation) {
164
return true;
165
}
166
167
const isGHE = capiClientService.dotcomAPIURL !== 'https://api.github.com';
168
const byokAllowed = (copilotToken.isInternal || copilotToken.isIndividual || copilotToken.isClientBYOKEnabled()) && !isGHE;
169
return byokAllowed;
170
}
171
172
/**
173
* Result of handling an API key update operation.
174
*/
175
export interface HandleAPIKeyUpdateResult {
176
/**
177
* The new API key value, or undefined if the key was deleted or operation was cancelled.
178
*/
179
apiKey: string | undefined;
180
/**
181
* Whether the API key was deleted (user entered empty string during reconfigure).
182
*/
183
deleted: boolean;
184
/**
185
* Whether the operation was cancelled (user dismissed the input).
186
*/
187
cancelled: boolean;
188
}
189
190
/**
191
* Storage service interface for BYOK API key operations.
192
* This is a minimal interface to avoid importing the full IBYOKStorageService in common code.
193
*/
194
export interface IBYOKStorageServiceLike {
195
getAPIKey(providerName: string, modelId?: string): Promise<string | undefined>;
196
storeAPIKey(providerName: string, apiKey: string, authType: BYOKAuthType, modelId?: string): Promise<void>;
197
deleteAPIKey(providerName: string, authType: BYOKAuthType, modelId?: string): Promise<void>;
198
}
199
200
/**
201
* Handles API key update flow for BYOK providers using a consistent pattern.
202
* This utility handles all three cases from promptForAPIKey:
203
* - undefined: user cancelled/dismissed the input
204
* - empty string: user wants to delete the saved key (only when reconfiguring)
205
* - non-empty string: user provided a new API key
206
*
207
* @param providerName - Name of the provider (e.g., 'Anthropic', 'Gemini')
208
* @param storageService - Storage service for API key operations
209
* @param promptForAPIKeyFn - Function to prompt user for API key
210
* @returns Result containing the new API key (if any) and status flags
211
*/
212
export async function handleAPIKeyUpdate(
213
providerName: string,
214
storageService: IBYOKStorageServiceLike,
215
promptForAPIKeyFn: (providerName: string, reconfigure: boolean) => Promise<string | undefined>
216
): Promise<HandleAPIKeyUpdateResult> {
217
const existingKey = await storageService.getAPIKey(providerName);
218
const isReconfiguring = existingKey !== undefined;
219
220
const newAPIKey = await promptForAPIKeyFn(providerName, isReconfiguring);
221
222
if (newAPIKey === undefined) {
223
// User cancelled/dismissed the input
224
return { apiKey: undefined, deleted: false, cancelled: true };
225
} else if (newAPIKey === '') {
226
// User wants to delete the key (only valid when reconfiguring)
227
await storageService.deleteAPIKey(providerName, BYOKAuthType.GlobalApiKey);
228
return { apiKey: undefined, deleted: true, cancelled: false };
229
} else {
230
// User provided a new API key
231
await storageService.storeAPIKey(providerName, newAPIKey, BYOKAuthType.GlobalApiKey);
232
return { apiKey: newAPIKey, deleted: false, cancelled: false };
233
}
234
}
235
236