Skip to content
13 changes: 0 additions & 13 deletions extensions/copilot/src/extension/byok/common/byokProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type { Disposable, LanguageModelChatInformation, LanguageModelDataPart, LanguageModelTextPart, LanguageModelThinkingPart, LanguageModelToolCallPart, LanguageModelToolResultPart } from 'vscode';
import { CopilotToken } from '../../../platform/authentication/common/copilotToken';
import { ICAPIClientService } from '../../../platform/endpoint/common/capiClient';
import { EndpointEditToolName, IChatModelInformation, ModelSupportedEndpoint } from '../../../platform/endpoint/common/endpointProvider';
import { isScenarioAutomation } from '../../../platform/env/common/envService';
import { TokenizerType } from '../../../util/common/tokenizer';

export const enum BYOKAuthType {
Expand Down Expand Up @@ -159,16 +156,6 @@ export function byokKnownModelToAPIInfo(providerName: string, id: string, capabi
};
}

export function isBYOKEnabled(copilotToken: Omit<CopilotToken, 'token'>, capiClientService: ICAPIClientService): boolean {
if (isScenarioAutomation) {
return true;
}

const isGHE = capiClientService.dotcomAPIURL !== 'https://api.github.com';
const byokAllowed = (copilotToken.isInternal || copilotToken.isIndividual || copilotToken.isClientBYOKEnabled()) && !isGHE;
return byokAllowed;
}

/**
* Result of handling an API key update operation.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { IBYOKStorageService } from './byokStorageService';
export class AnthropicLMProvider extends AbstractLanguageModelChatProvider {

public static readonly providerName = 'Anthropic';
public static readonly providerId = 'anthropic';

constructor(
knownModels: BYOKKnownModels | undefined,
Expand All @@ -46,7 +47,7 @@ export class AnthropicLMProvider extends AbstractLanguageModelChatProvider {
@IOTelService private readonly _otelService: IOTelService,
@IToolDeferralService private readonly _toolDeferralService: IToolDeferralService,
) {
super(AnthropicLMProvider.providerName.toLowerCase(), AnthropicLMProvider.providerName, knownModels, byokStorageService, logService);
super(AnthropicLMProvider.providerId, AnthropicLMProvider.providerName, knownModels, byokStorageService, logService);

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function resolveAzureUrl(modelId: string, url: string): string {
export class AzureBYOKModelProvider extends AbstractCustomOAIBYOKModelProvider {

static readonly providerName = 'Azure';
static readonly providerId = 'azure';

constructor(
byokStorageService: IBYOKStorageService,
Expand All @@ -59,7 +60,7 @@ export class AzureBYOKModelProvider extends AbstractCustomOAIBYOKModelProvider {
@IVSCodeExtensionContext extensionContext: IVSCodeExtensionContext
) {
super(
AzureBYOKModelProvider.providerName.toLowerCase(),
AzureBYOKModelProvider.providerId,
AzureBYOKModelProvider.providerName,
byokStorageService,
logService,
Expand Down
138 changes: 91 additions & 47 deletions extensions/copilot/src/extension/byok/vscode-node/byokContribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { LanguageModelChatInformation, LanguageModelChatProvider, lm } from 'vscode';
import { IAuthenticationService } from '../../../platform/authentication/common/authentication';
import { ICAPIClientService } from '../../../platform/endpoint/common/capiClient';
import { commands, LanguageModelChatInformation, LanguageModelChatProvider, lm } from 'vscode';
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
import { ILogService } from '../../../platform/log/common/logService';
import { IFetcherService } from '../../../platform/networking/common/fetcherService';
import { Disposable, DisposableStore } from '../../../util/vs/base/common/lifecycle';
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
import { BYOKKnownModels, isBYOKEnabled } from '../../byok/common/byokProvider';
import { BYOKKnownModels } from '../../byok/common/byokProvider';
import { IExtensionContribution } from '../../common/contributions';
import { AnthropicLMProvider } from './anthropicProvider';
import { AzureBYOKModelProvider } from './azureProvider';
Expand All @@ -22,6 +20,19 @@ import { OAIBYOKLMProvider } from './openAIProvider';
import { OpenRouterLMProvider } from './openRouterProvider';
import { XAIBYOKLMProvider } from './xAIProvider';

export const hasByokModelsContextKey = 'github.copilot.hasByokModels';

export const byokVendorIds = [
OllamaLMProvider.providerId,
AnthropicLMProvider.providerId,
GeminiNativeBYOKLMProvider.providerId,
XAIBYOKLMProvider.providerId,
OAIBYOKLMProvider.providerId,
OpenRouterLMProvider.providerId,
AzureBYOKModelProvider.providerId,
CustomOAIBYOKModelProvider.providerId,
];

export class BYOKContrib extends Disposable implements IExtensionContribution {
public readonly id: string = 'byok-contribution';
private readonly _byokStorageService: IBYOKStorageService;
Expand All @@ -32,64 +43,97 @@ export class BYOKContrib extends Disposable implements IExtensionContribution {
constructor(
@IFetcherService private readonly _fetcherService: IFetcherService,
@ILogService private readonly _logService: ILogService,
@ICAPIClientService private readonly _capiClientService: ICAPIClientService,
@IVSCodeExtensionContext extensionContext: IVSCodeExtensionContext,
@IAuthenticationService authService: IAuthenticationService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
) {
super();
this._byokStorageService = new BYOKStorageService(extensionContext);
this._authChange(authService, this._instantiationService);

this._register(authService.onDidAuthenticationChange(() => {
this._authChange(authService, this._instantiationService);
}));
void this._registerProviders().catch(err => {
this._byokProvidersRegistered = false;
this._logService.error(err instanceof Error ? err : String(err), 'BYOK: Failed to register providers.');
});
Comment thread
dmitrivMS marked this conversation as resolved.
}

private async _authChange(authService: IAuthenticationService, instantiationService: IInstantiationService) {
const byokEnabled = authService.copilotToken && isBYOKEnabled(authService.copilotToken, this._capiClientService);
private async _registerProviders() {
if (this._byokProvidersRegistered) {
return;
}

if (!byokEnabled && this._byokProvidersRegistered) {
this._logService.info('BYOK: Disabling BYOK providers due to account change.');
this._byokRegistrations.clear();
this._providers.clear();
this._byokProvidersRegistered = false;
this._byokProvidersRegistered = true;
const instantiationService = this._instantiationService;

// Fetch known models from CDN for model metadata (capabilities, token limits).
// Uses a timeout to avoid blocking provider registration in air-gapped/offline environments.
const knownModels = await this._fetchKnownModelListWithTimeout(this._fetcherService);
if (this._store.isDisposed) {
Comment thread
dmitrivMS marked this conversation as resolved.
return;
}

if (byokEnabled && !this._byokProvidersRegistered) {
this._byokProvidersRegistered = true;
// Update known models list from CDN so all providers have the same list
const knownModels = await this.fetchKnownModelList(this._fetcherService);
if (this._store.isDisposed) {
return;
}
this._providers.set(OllamaLMProvider.providerName.toLowerCase(), instantiationService.createInstance(OllamaLMProvider, this._byokStorageService));
this._providers.set(AnthropicLMProvider.providerName.toLowerCase(), instantiationService.createInstance(AnthropicLMProvider, knownModels[AnthropicLMProvider.providerName], this._byokStorageService));
this._providers.set(GeminiNativeBYOKLMProvider.providerName.toLowerCase(), instantiationService.createInstance(GeminiNativeBYOKLMProvider, knownModels[GeminiNativeBYOKLMProvider.providerName], this._byokStorageService));
this._providers.set(XAIBYOKLMProvider.providerName.toLowerCase(), instantiationService.createInstance(XAIBYOKLMProvider, knownModels[XAIBYOKLMProvider.providerName], this._byokStorageService));
this._providers.set(OAIBYOKLMProvider.providerName.toLowerCase(), instantiationService.createInstance(OAIBYOKLMProvider, knownModels[OAIBYOKLMProvider.providerName], this._byokStorageService));
this._providers.set(OpenRouterLMProvider.providerName.toLowerCase(), instantiationService.createInstance(OpenRouterLMProvider, this._byokStorageService));
this._providers.set(AzureBYOKModelProvider.providerName.toLowerCase(), instantiationService.createInstance(AzureBYOKModelProvider, this._byokStorageService));
this._providers.set(CustomOAIBYOKModelProvider.providerName.toLowerCase(), instantiationService.createInstance(CustomOAIBYOKModelProvider, this._byokStorageService));
this._providers.set(OllamaLMProvider.providerId, instantiationService.createInstance(OllamaLMProvider, this._byokStorageService));
this._providers.set(AnthropicLMProvider.providerId, instantiationService.createInstance(AnthropicLMProvider, knownModels[AnthropicLMProvider.providerName], this._byokStorageService));
this._providers.set(GeminiNativeBYOKLMProvider.providerId, instantiationService.createInstance(GeminiNativeBYOKLMProvider, knownModels[GeminiNativeBYOKLMProvider.providerName], this._byokStorageService));
this._providers.set(XAIBYOKLMProvider.providerId, instantiationService.createInstance(XAIBYOKLMProvider, knownModels[XAIBYOKLMProvider.providerName], this._byokStorageService));
this._providers.set(OAIBYOKLMProvider.providerId, instantiationService.createInstance(OAIBYOKLMProvider, knownModels[OAIBYOKLMProvider.providerName], this._byokStorageService));
this._providers.set(OpenRouterLMProvider.providerId, instantiationService.createInstance(OpenRouterLMProvider, this._byokStorageService));
this._providers.set(AzureBYOKModelProvider.providerId, instantiationService.createInstance(AzureBYOKModelProvider, this._byokStorageService));
this._providers.set(CustomOAIBYOKModelProvider.providerId, instantiationService.createInstance(CustomOAIBYOKModelProvider, this._byokStorageService));

for (const [providerName, provider] of this._providers) {
this._byokRegistrations.add(lm.registerLanguageModelChatProvider(providerName, provider));
for (const [providerName, provider] of this._providers) {
this._byokRegistrations.add(lm.registerLanguageModelChatProvider(providerName, provider));
}
Comment thread
dmitrivMS marked this conversation as resolved.

await this._updateHasByokModelsContext();

// Update context key when language models change (e.g., model configured/removed)
this._register(lm.onDidChangeChatModels(() => {
void this._updateHasByokModelsContext().catch(err => {
this._logService.error(err instanceof Error ? err : String(err), 'BYOK: Failed to update BYOK models context.');
});
}));
}

async _updateHasByokModelsContext(): Promise<void> {
try {
let hasModels = false;
for (const vendor of this._providers.keys()) {
const models = await lm.selectChatModels({ vendor });
if (models.length > 0) {
hasModels = true;
break;
}
}
commands.executeCommand('setContext', hasByokModelsContextKey, hasModels);
} catch (err) {
this._logService.error(err instanceof Error ? err : String(err), 'BYOK: Failed to update BYOK models context.');
commands.executeCommand('setContext', hasByokModelsContextKey, false);
}
}
Comment on lines 43 to 110
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BYOK model configuration is stored in extension globalState (not account-scoped), but this contribution now registers providers unconditionally and derives github.copilot.hasByokModels purely from lm.selectChatModels. This means BYOK models configured under a previous account can still make hasByokModels true after switching to an enterprise-managed account where BYOK is disabled, which can bypass setup/sign-in UI and potentially violate org BYOK policy. Consider scoping BYOK storage by account/org (or clearing/ignoring stored models on auth change for managed accounts), and/or incorporating the current BYOK policy signal (clientByokEnabled/token) when computing hasByokModels and exposing models.

This issue also appears on line 101 of the same file.

Copilot uses AI. Check for mistakes.

private async _fetchKnownModelListWithTimeout(fetcherService: IFetcherService): Promise<Record<string, BYOKKnownModels>> {
const CDN_FETCH_TIMEOUT_MS = 5000;
return Promise.race([
this.fetchKnownModelList(fetcherService),
new Promise<Record<string, BYOKKnownModels>>(resolve => setTimeout(() => {
this._logService.warn('BYOK: CDN fetch timed out. Registering providers with empty known models list.');
resolve({});
}, CDN_FETCH_TIMEOUT_MS))
]);
}

private async fetchKnownModelList(fetcherService: IFetcherService): Promise<Record<string, BYOKKnownModels>> {
const data = await (await fetcherService.fetch('https://main.vscode-cdn.net/extensions/copilotChat.json', { method: 'GET', callSite: 'byok-known-models' })).json();
// Use this for testing with changes from a local file. Don't check in
// const data = JSON.parse((await this._fileSystemService.readFile(URI.file('/Users/roblou/code/vscode-engineering/chat/copilotChat.json'))).toString());
let knownModels: Record<string, BYOKKnownModels>;
if (data.version !== 1) {
this._logService.warn('BYOK: Copilot Chat known models list is not in the expected format. Defaulting to empty list.');
knownModels = {};
} else {
knownModels = data.modelInfo;
try {
const data = await (await fetcherService.fetch('https://main.vscode-cdn.net/extensions/copilotChat.json', { method: 'GET', callSite: 'byok-known-models' })).json();
// Use this for testing with changes from a local file. Don't check in
// const data = JSON.parse((await this._fileSystemService.readFile(URI.file('/Users/roblou/code/vscode-engineering/chat/copilotChat.json'))).toString());
if (data.version !== 1) {
this._logService.warn('BYOK: Copilot Chat known models list is not in the expected format. Defaulting to empty list.');
return {};
}
this._logService.info('BYOK: Copilot Chat known models list fetched successfully.');
return data.modelInfo;
} catch (err) {
this._logService.warn(`BYOK: Failed to fetch known models list. Defaulting to empty list. ${err}`);
return {};
}
this._logService.info('BYOK: Copilot Chat known models list fetched successfully.');
return knownModels;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export abstract class AbstractCustomOAIBYOKModelProvider extends AbstractOpenAIC
export class CustomOAIBYOKModelProvider extends AbstractCustomOAIBYOKModelProvider {

static readonly providerName: string = 'CustomOAI';
static readonly providerId = 'customoai';
private providerName: string = CustomOAIBYOKModelProvider.providerName;

constructor(
Expand All @@ -175,7 +176,7 @@ export class CustomOAIBYOKModelProvider extends AbstractCustomOAIBYOKModelProvid
@IExperimentationService expService: IExperimentationService,
@IVSCodeExtensionContext extensionContext: IVSCodeExtensionContext
) {
super(CustomOAIBYOKModelProvider.providerName.toLowerCase(), CustomOAIBYOKModelProvider.providerName, _byokStorageService, logService, fetcherService, instantiationService, configurationService, expService, extensionContext);
super(CustomOAIBYOKModelProvider.providerId, CustomOAIBYOKModelProvider.providerName, _byokStorageService, logService, fetcherService, instantiationService, configurationService, expService, extensionContext);
this.migrateExistingConfigs();
}

Expand All @@ -187,4 +188,4 @@ export class CustomOAIBYOKModelProvider extends AbstractCustomOAIBYOKModelProvid
protected resolveUrl(modelId: string, url: string): string {
return resolveCustomOAIUrl(modelId, url);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { IBYOKStorageService } from './byokStorageService';
export class GeminiNativeBYOKLMProvider extends AbstractLanguageModelChatProvider {

public static readonly providerName = 'Gemini';
public static readonly providerId = 'gemini';

constructor(
knownModels: BYOKKnownModels | undefined,
Expand All @@ -35,7 +36,7 @@ export class GeminiNativeBYOKLMProvider extends AbstractLanguageModelChatProvide
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@IOTelService private readonly _otelService: IOTelService,
) {
super(GeminiNativeBYOKLMProvider.providerName.toLowerCase(), GeminiNativeBYOKLMProvider.providerName, knownModels, byokStorageService, logService);
super(GeminiNativeBYOKLMProvider.providerId, GeminiNativeBYOKLMProvider.providerName, knownModels, byokStorageService, logService);
}

protected async getAllModels(silent: boolean, apiKey: string | undefined): Promise<ExtendedLanguageModelChatInformation<LanguageModelChatConfiguration>[]> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface OllamaConfig extends LanguageModelChatConfiguration {

export class OllamaLMProvider extends AbstractOpenAICompatibleLMProvider<OllamaConfig> {
public static readonly providerName = 'Ollama';
public static readonly providerId = 'ollama';
private _modelCache = new Map<string, IChatModelInformation>();

constructor(
Expand All @@ -50,7 +51,7 @@ export class OllamaLMProvider extends AbstractOpenAICompatibleLMProvider<OllamaC
@IExperimentationService expService: IExperimentationService
) {
super(
OllamaLMProvider.providerName.toLowerCase(),
OllamaLMProvider.providerId,
OllamaLMProvider.providerName,
undefined,
byokStorageService,
Expand Down Expand Up @@ -224,4 +225,4 @@ export class OllamaLMProvider extends AbstractOpenAICompatibleLMProvider<OllamaC
);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { IBYOKStorageService } from './byokStorageService';

export class OAIBYOKLMProvider extends AbstractOpenAICompatibleLMProvider {
public static readonly providerName = 'OpenAI';
public static readonly providerId = 'openai';

constructor(
knownModels: BYOKKnownModels,
Expand All @@ -25,7 +26,7 @@ export class OAIBYOKLMProvider extends AbstractOpenAICompatibleLMProvider {
@IExperimentationService expService: IExperimentationService
) {
super(
OAIBYOKLMProvider.providerName.toLowerCase(),
OAIBYOKLMProvider.providerId,
OAIBYOKLMProvider.providerName,
knownModels,
byokStorageService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface OpenRouterModelData {

export class OpenRouterLMProvider extends AbstractOpenAICompatibleLMProvider {
public static readonly providerName = 'OpenRouter';
public static readonly providerId = 'openrouter';
constructor(
byokStorageService: IBYOKStorageService,
@IFetcherService fetcherService: IFetcherService,
Expand All @@ -34,7 +35,7 @@ export class OpenRouterLMProvider extends AbstractOpenAICompatibleLMProvider {
@IExperimentationService expService: IExperimentationService
) {
super(
OpenRouterLMProvider.providerName.toLowerCase(),
OpenRouterLMProvider.providerId,
OpenRouterLMProvider.providerName,
undefined,
byokStorageService,
Expand Down Expand Up @@ -65,4 +66,4 @@ export class OpenRouterLMProvider extends AbstractOpenAICompatibleLMProvider {
};
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface XAIModelData {
export class XAIBYOKLMProvider extends AbstractOpenAICompatibleLMProvider {

public static readonly providerName = 'xAI';
public static readonly providerId = 'xai';

constructor(
knownModels: BYOKKnownModels,
Expand All @@ -43,7 +44,7 @@ export class XAIBYOKLMProvider extends AbstractOpenAICompatibleLMProvider {
@IExperimentationService expService: IExperimentationService
) {
super(
XAIBYOKLMProvider.providerName.toLowerCase(),
XAIBYOKLMProvider.providerId,
XAIBYOKLMProvider.providerName,
knownModels,
byokStorageService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,14 @@ export class ContextKeysContribution extends Disposable {
private async _updateClientByokEnabledContext() {
try {
const copilotToken = await this._authenticationService.getCopilotToken();
commands.executeCommand('setContext', clientByokEnabledContextKey, copilotToken.isClientBYOKEnabled());
// When signed in, respect enterprise BYOK policy:
// internal and individual users always have BYOK enabled,
// managed (enterprise/business) users require explicit org enablement.
const byokEnabled = copilotToken.isInternal || copilotToken.isIndividual || copilotToken.isClientBYOKEnabled();
commands.executeCommand('setContext', clientByokEnabledContextKey, byokEnabled);
} catch (e) {
commands.executeCommand('setContext', clientByokEnabledContextKey, undefined);
// When not signed in, BYOK is available by default
commands.executeCommand('setContext', clientByokEnabledContextKey, true);
Comment on lines +221 to +222
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_updateClientByokEnabledContext treats any failure to mint/read a Copilot token as "not signed in" and sets github.copilot.clientByokEnabled to true. Because getCopilotToken() can throw for reasons other than signed-out (e.g. transient token minting/network errors, subscription errors), this can temporarily/incorrectly enable BYOK UI and bypass enterprise policy checks. Consider distinguishing "no session" from other failures (e.g. use cached copilotToken/session state when available, or preserve the previous context value on error) instead of unconditionally defaulting to true in the catch block.

Suggested change
// When not signed in, BYOK is available by default
commands.executeCommand('setContext', clientByokEnabledContextKey, true);
const cachedCopilotToken = this._authenticationService.copilotToken;
if (cachedCopilotToken) {
const byokEnabled = cachedCopilotToken.isInternal || cachedCopilotToken.isIndividual || cachedCopilotToken.isClientBYOKEnabled();
commands.executeCommand('setContext', clientByokEnabledContextKey, byokEnabled);
return;
}
this._logService.trace(`[context keys] Failed to resolve client BYOK context: ${e instanceof Error ? e.message : String(e)}`);
// Preserve the existing context value when token resolution fails without cached state.

Copilot uses AI. Check for mistakes.
}
}

Expand Down
Loading
Loading