-
Notifications
You must be signed in to change notification settings - Fork 39.7k
Enable BYOK without authentication #312207
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6b79daf
068fa20
6c1f720
8e4478f
3d0f14f
d9c9380
44bd6d6
3c61bca
83d1b2e
dc354a3
e146b4a
a8547cf
7258316
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'; | ||
|
|
@@ -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; | ||
|
|
@@ -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.'); | ||
| }); | ||
| } | ||
|
|
||
| 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) { | ||
|
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)); | ||
| } | ||
|
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
|
||
|
|
||
| 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 | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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
|
||||||||||||||||||||||||
| // 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. |
Uh oh!
There was an error while loading. Please reload this page.