Path: blob/main/extensions/copilot/src/extension/byok/common/byokProvider.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*--------------------------------------------------------------------------------------------*/4import type { Disposable, LanguageModelChatInformation, LanguageModelDataPart, LanguageModelTextPart, LanguageModelThinkingPart, LanguageModelToolCallPart, LanguageModelToolResultPart } from 'vscode';5import { CopilotToken } from '../../../platform/authentication/common/copilotToken';6import { ICAPIClientService } from '../../../platform/endpoint/common/capiClient';7import { EndpointEditToolName, IChatModelInformation, ModelSupportedEndpoint } from '../../../platform/endpoint/common/endpointProvider';8import { isScenarioAutomation } from '../../../platform/env/common/envService';9import { TokenizerType } from '../../../util/common/tokenizer';1011export const enum BYOKAuthType {12/**13* Requires a single API key for all models (e.g., OpenAI)14*/15GlobalApiKey,16/**17* Requires both deployment URL and API key per model (e.g., Azure)18*/19PerModelDeployment,20/**21* No authentication required (e.g., Ollama)22*/23None24}2526interface BYOKBaseModelConfig {27modelId: string;28capabilities?: BYOKModelCapabilities;29}3031export type LMResponsePart = LanguageModelTextPart | LanguageModelToolCallPart | LanguageModelDataPart | LanguageModelThinkingPart | LanguageModelToolResultPart;3233export interface BYOKGlobalKeyModelConfig extends BYOKBaseModelConfig {34apiKey: string;35}3637export interface BYOKPerModelConfig extends BYOKBaseModelConfig {38apiKey: string;39deploymentUrl: string;40}4142interface BYOKNoAuthModelConfig extends BYOKBaseModelConfig {43// No additional fields required44}4546export type BYOKModelConfig = BYOKGlobalKeyModelConfig | BYOKPerModelConfig | BYOKNoAuthModelConfig;4748export interface BYOKModelCapabilities {49name: string;50url?: string;51maxInputTokens: number;52maxOutputTokens: number;53toolCalling: boolean;54vision: boolean;55thinking?: boolean;56adaptiveThinking?: boolean;57streaming?: boolean;58editTools?: EndpointEditToolName[];59requestHeaders?: Record<string, string>;60supportedEndpoints?: ModelSupportedEndpoint[];61zeroDataRetentionEnabled?: boolean;62supportsReasoningEffort?: string[];63}6465export interface BYOKModelRegistry {66readonly name: string;67readonly authType: BYOKAuthType;68updateKnownModelsList(knownModels: BYOKKnownModels | undefined): void;69getAllModels(apiKey?: string): Promise<{ id: string; name: string }[]>;70registerModel(config: BYOKModelConfig): Promise<Disposable>;71}7273// 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 id74export type BYOKKnownModels = Record<string, BYOKModelCapabilities>;7576// Type guards to ensure correct config type77export function isGlobalKeyConfig(config: BYOKModelConfig): config is BYOKGlobalKeyModelConfig {78return 'apiKey' in config && !('deploymentUrl' in config);79}8081export function isPerModelConfig(config: BYOKModelConfig): config is BYOKPerModelConfig {82return 'apiKey' in config && 'deploymentUrl' in config;83}8485export function isNoAuthConfig(config: BYOKModelConfig): config is BYOKNoAuthModelConfig {86return !('apiKey' in config) && !('deploymentUrl' in config);87}8889export function resolveModelInfo(modelId: string, providerName: string, knownModels: BYOKKnownModels | undefined, modelCapabilities?: BYOKModelCapabilities): IChatModelInformation {90// Model Capabilities are something the user has decided on so those take precedence, then we rely on known model info, then defaults.91let knownModelInfo = modelCapabilities;92if (knownModels && !knownModelInfo) {93knownModelInfo = knownModels[modelId];94}95const modelName = knownModelInfo?.name || modelId;96const contextWinow = knownModelInfo ? (knownModelInfo.maxInputTokens + knownModelInfo.maxOutputTokens) : 128000;97const modelInfo: IChatModelInformation = {98id: modelId,99name: modelName,100vendor: providerName,101version: '1.0.0',102capabilities: {103type: 'chat',104family: modelId,105supports: {106streaming: knownModelInfo?.streaming ?? true,107tool_calls: !!knownModelInfo?.toolCalling,108vision: !!knownModelInfo?.vision,109thinking: !!knownModelInfo?.thinking,110adaptive_thinking: !!knownModelInfo?.adaptiveThinking111},112tokenizer: TokenizerType.O200K,113limits: {114max_context_window_tokens: contextWinow,115max_prompt_tokens: knownModelInfo?.maxInputTokens || 100000,116max_output_tokens: knownModelInfo?.maxOutputTokens || 8192117}118},119is_chat_default: false,120is_chat_fallback: false,121model_picker_enabled: true,122supported_endpoints: knownModelInfo?.supportedEndpoints,123zeroDataRetentionEnabled: knownModelInfo?.zeroDataRetentionEnabled124};125if (knownModelInfo?.requestHeaders && Object.keys(knownModelInfo.requestHeaders).length > 0) {126modelInfo.requestHeaders = { ...knownModelInfo.requestHeaders };127}128return modelInfo;129}130131export function byokKnownModelsToAPIInfo(providerName: string, knownModels: BYOKKnownModels | undefined): LanguageModelChatInformation[] {132if (!knownModels) {133return [];134}135return Object.entries(knownModels).map(([id, capabilities]) => byokKnownModelToAPIInfo(providerName, id, capabilities));136}137138export function byokKnownModelToAPIInfo(providerName: string, id: string, capabilities: BYOKModelCapabilities): LanguageModelChatInformation {139return {140id,141name: capabilities.name,142version: '1.0.0',143maxOutputTokens: capabilities.maxOutputTokens,144maxInputTokens: capabilities.maxInputTokens,145// `detail` is intentionally omitted: when this model is resolved146// via a configured provider group, `LanguageModelsService` will147// fall back to the group name so multiple instances of the same148// vendor (e.g. multiple Ollama servers) are distinguishable in149// the model picker.150family: id,151tooltip: `${capabilities.name} is contributed via the ${providerName} provider.`,152multiplierNumeric: 0,153isUserSelectable: true,154capabilities: {155toolCalling: capabilities.toolCalling,156imageInput: capabilities.vision157},158};159}160161export function isBYOKEnabled(copilotToken: Omit<CopilotToken, 'token'>, capiClientService: ICAPIClientService): boolean {162if (isScenarioAutomation) {163return true;164}165166const isGHE = capiClientService.dotcomAPIURL !== 'https://api.github.com';167const byokAllowed = (copilotToken.isInternal || copilotToken.isIndividual || copilotToken.isClientBYOKEnabled()) && !isGHE;168return byokAllowed;169}170171/**172* Result of handling an API key update operation.173*/174export interface HandleAPIKeyUpdateResult {175/**176* The new API key value, or undefined if the key was deleted or operation was cancelled.177*/178apiKey: string | undefined;179/**180* Whether the API key was deleted (user entered empty string during reconfigure).181*/182deleted: boolean;183/**184* Whether the operation was cancelled (user dismissed the input).185*/186cancelled: boolean;187}188189/**190* Storage service interface for BYOK API key operations.191* This is a minimal interface to avoid importing the full IBYOKStorageService in common code.192*/193export interface IBYOKStorageServiceLike {194getAPIKey(providerName: string, modelId?: string): Promise<string | undefined>;195storeAPIKey(providerName: string, apiKey: string, authType: BYOKAuthType, modelId?: string): Promise<void>;196deleteAPIKey(providerName: string, authType: BYOKAuthType, modelId?: string): Promise<void>;197}198199/**200* Handles API key update flow for BYOK providers using a consistent pattern.201* This utility handles all three cases from promptForAPIKey:202* - undefined: user cancelled/dismissed the input203* - empty string: user wants to delete the saved key (only when reconfiguring)204* - non-empty string: user provided a new API key205*206* @param providerName - Name of the provider (e.g., 'Anthropic', 'Gemini')207* @param storageService - Storage service for API key operations208* @param promptForAPIKeyFn - Function to prompt user for API key209* @returns Result containing the new API key (if any) and status flags210*/211export async function handleAPIKeyUpdate(212providerName: string,213storageService: IBYOKStorageServiceLike,214promptForAPIKeyFn: (providerName: string, reconfigure: boolean) => Promise<string | undefined>215): Promise<HandleAPIKeyUpdateResult> {216const existingKey = await storageService.getAPIKey(providerName);217const isReconfiguring = existingKey !== undefined;218219const newAPIKey = await promptForAPIKeyFn(providerName, isReconfiguring);220221if (newAPIKey === undefined) {222// User cancelled/dismissed the input223return { apiKey: undefined, deleted: false, cancelled: true };224} else if (newAPIKey === '') {225// User wants to delete the key (only valid when reconfiguring)226await storageService.deleteAPIKey(providerName, BYOKAuthType.GlobalApiKey);227return { apiKey: undefined, deleted: true, cancelled: false };228} else {229// User provided a new API key230await storageService.storeAPIKey(providerName, newAPIKey, BYOKAuthType.GlobalApiKey);231return { apiKey: newAPIKey, deleted: false, cancelled: false };232}233}234235236