1+ using CrestApps . OrchardCore . AI . Chat . Copilot . Models ;
12using CrestApps . OrchardCore . AI . Chat . Copilot . Settings ;
23using CrestApps . OrchardCore . AI . Chat . Copilot . ViewModels ;
34using CrestApps . OrchardCore . AI . Core ;
45using Microsoft . AspNetCore . Authorization ;
56using Microsoft . AspNetCore . DataProtection ;
67using Microsoft . AspNetCore . Http ;
78using Microsoft . AspNetCore . Mvc . Localization ;
9+ using Microsoft . AspNetCore . Mvc . Rendering ;
810using Microsoft . AspNetCore . Routing ;
911using Microsoft . Extensions . Localization ;
1012using OrchardCore . DisplayManagement . Entities ;
@@ -48,12 +50,41 @@ public override IDisplayResult Edit(ISite site, CopilotSettings settings, BuildE
4850 {
4951 return Initialize < CopilotSettingsViewModel > ( "CopilotSettings_Edit" , model =>
5052 {
53+ model . AuthenticationType = settings . AuthenticationType ;
5154 model . ClientId = settings . ClientId ;
5255 model . HasSecret = ! string . IsNullOrWhiteSpace ( settings . ProtectedClientSecret ) ;
5356 model . ComputedCallbackUrl = _linkGenerator . GetUriByAction ( _httpContextAccessor . HttpContext , "OAuthCallback" , "CopilotAuth" , new
5457 {
5558 area = "CrestApps.OrchardCore.AI.Chat.Copilot" ,
5659 } ) ;
60+
61+ // BYOK fields
62+ model . ProviderType = settings . ProviderType ;
63+ model . BaseUrl = settings . BaseUrl ;
64+ model . HasApiKey = ! string . IsNullOrWhiteSpace ( settings . ProtectedApiKey ) ;
65+ model . WireApi = settings . WireApi ?? "completions" ;
66+ model . DefaultModel = settings . DefaultModel ;
67+ model . AzureApiVersion = settings . AzureApiVersion ;
68+
69+ // Select list options
70+ model . AuthenticationTypes =
71+ [
72+ new SelectListItem ( S [ "GitHub Signed-in User" ] , nameof ( CopilotAuthenticationType . GitHubOAuth ) ) ,
73+ new SelectListItem ( S [ "API Key (BYOK)" ] , nameof ( CopilotAuthenticationType . ApiKey ) ) ,
74+ ] ;
75+
76+ model . ProviderTypes =
77+ [
78+ new SelectListItem ( S [ "OpenAI / OpenAI-compatible (Ollama, vLLM, etc.)" ] , "openai" ) ,
79+ new SelectListItem ( S [ "Azure OpenAI" ] , "azure" ) ,
80+ new SelectListItem ( S [ "Anthropic" ] , "anthropic" ) ,
81+ ] ;
82+
83+ model . WireApiOptions =
84+ [
85+ new SelectListItem ( S [ "Chat Completions (default)" ] , "completions" ) ,
86+ new SelectListItem ( S [ "Responses (GPT-5 series)" ] , "responses" ) ,
87+ ] ;
5788 } )
5889 . Location ( "Content:8%Copilot;1" )
5990 . OnGroup ( SettingsGroupId )
@@ -66,24 +97,70 @@ public override async Task<IDisplayResult> UpdateAsync(ISite site, CopilotSettin
6697
6798 await context . Updater . TryUpdateModelAsync ( model , Prefix ) ;
6899
69- settings . ClientId = model . ClientId ;
100+ settings . AuthenticationType = model . AuthenticationType ;
70101
71- // Validate that client ID and secret are provided
72- if ( string . IsNullOrWhiteSpace ( settings . ClientId ) )
102+ if ( settings . AuthenticationType == CopilotAuthenticationType . GitHubOAuth )
73103 {
74- context . Updater . ModelState . AddModelError ( nameof ( model . ClientId ) , S [ "Client ID is required." ] ) ;
75- }
104+ // GitHub OAuth validation
105+ settings . ClientId = model . ClientId ;
76106
77- // Only update the secret if a new one was provided
78- if ( ! string . IsNullOrWhiteSpace ( model . ClientSecret ) )
79- {
80- var protector = _dataProtectionProvider . CreateProtector ( ProtectorPurpose ) ;
81- settings . ProtectedClientSecret = protector . Protect ( model . ClientSecret ) ;
107+ if ( string . IsNullOrWhiteSpace ( settings . ClientId ) )
108+ {
109+ context . Updater . ModelState . AddModelError ( nameof ( model . ClientId ) , S [ "Client ID is required." ] ) ;
110+ }
111+
112+ if ( ! string . IsNullOrWhiteSpace ( model . ClientSecret ) )
113+ {
114+ var protector = _dataProtectionProvider . CreateProtector ( ProtectorPurpose ) ;
115+ settings . ProtectedClientSecret = protector . Protect ( model . ClientSecret ) ;
116+ }
117+ else if ( string . IsNullOrWhiteSpace ( settings . ProtectedClientSecret ) )
118+ {
119+ context . Updater . ModelState . AddModelError ( nameof ( model . ClientSecret ) , S [ "Client Secret is required." ] ) ;
120+ }
82121 }
83- else if ( string . IsNullOrWhiteSpace ( settings . ProtectedClientSecret ) )
122+ else
84123 {
85- // No existing secret and no new secret provided
86- context . Updater . ModelState . AddModelError ( nameof ( model . ClientSecret ) , S [ "Client Secret is required." ] ) ;
124+ // BYOK (API Key) validation
125+ settings . ProviderType = model . ProviderType ;
126+ settings . BaseUrl = model . BaseUrl ;
127+ settings . WireApi = model . WireApi ;
128+ settings . DefaultModel = model . DefaultModel ;
129+ settings . AzureApiVersion = model . AzureApiVersion ;
130+
131+ if ( string . IsNullOrWhiteSpace ( settings . ProviderType ) )
132+ {
133+ context . Updater . ModelState . AddModelError ( nameof ( model . ProviderType ) , S [ "Provider Type is required." ] ) ;
134+ }
135+
136+ if ( string . IsNullOrWhiteSpace ( settings . BaseUrl ) )
137+ {
138+ context . Updater . ModelState . AddModelError ( nameof ( model . BaseUrl ) , S [ "Base URL is required." ] ) ;
139+ }
140+
141+ if ( string . IsNullOrWhiteSpace ( settings . DefaultModel ) )
142+ {
143+ context . Updater . ModelState . AddModelError ( nameof ( model . DefaultModel ) , S [ "Default Model is required." ] ) ;
144+ }
145+
146+ if ( ! string . IsNullOrWhiteSpace ( model . ApiKey ) )
147+ {
148+ var protector = _dataProtectionProvider . CreateProtector ( ProtectorPurpose ) ;
149+ settings . ProtectedApiKey = protector . Protect ( model . ApiKey ) ;
150+ }
151+
152+ if ( string . Equals ( settings . ProviderType , "azure" , StringComparison . OrdinalIgnoreCase )
153+ && string . IsNullOrWhiteSpace ( settings . AzureApiVersion ) )
154+ {
155+ context . Updater . ModelState . AddModelError ( nameof ( model . AzureApiVersion ) , S [ "Azure API Version is required for Azure provider." ] ) ;
156+ }
157+
158+ if ( string . Equals ( settings . ProviderType , "azure" , StringComparison . OrdinalIgnoreCase )
159+ && string . IsNullOrWhiteSpace ( model . ApiKey )
160+ && string . IsNullOrWhiteSpace ( settings . ProtectedApiKey ) )
161+ {
162+ context . Updater . ModelState . AddModelError ( nameof ( model . ApiKey ) , S [ "API Key is required for Azure provider." ] ) ;
163+ }
87164 }
88165
89166 return await EditAsync ( site , settings , context ) ;
0 commit comments