fix: migrate Custom OpenAI services to use UUIDs and several other fixes
Some checks failed
Build / Build (push) Has been cancelled
Build / Verify Plugin (push) Has been cancelled

This commit is contained in:
Carl-Robert Linnupuu 2025-09-16 23:44:34 +01:00
parent fdfde4243d
commit 163758a2be
27 changed files with 527 additions and 380 deletions

View file

@ -58,13 +58,32 @@ public class CustomServiceFormTabbedPane extends JBTabbedPane {
private void setTableData(JBTable table, Map<String, ?> values) {
DefaultTableModel model = (DefaultTableModel) table.getModel();
model.setRowCount(0);
for (var entry : values.entrySet()) {
model.addRow(new Object[]{entry.getKey(), entry.getValue()});
if (hasTableDataChanged(model, values)) {
model.setRowCount(0);
for (var entry : values.entrySet()) {
model.addRow(new Object[]{entry.getKey(), entry.getValue()});
}
}
}
private boolean hasTableDataChanged(DefaultTableModel model, Map<String, ?> newValues) {
if (model.getRowCount() != newValues.size()) {
return true;
}
for (int i = 0; i < model.getRowCount(); i++) {
String key = (String) model.getValueAt(i, 0);
Object value = model.getValueAt(i, 1);
if (!newValues.containsKey(key) || !java.util.Objects.equals(newValues.get(key), value)) {
return true;
}
}
return false;
}
private Map<String, Object> getTableData(JBTable table) {
var model = (DefaultTableModel) table.getModel();
var data = new HashMap<String, Object>();

View file

@ -312,11 +312,11 @@ public class ModelComboBoxAction extends ComboBoxAction {
break;
case CUSTOM_OPENAI:
ModelRegistry.getInstance().getCustomOpenAIModels().stream()
.filter(it -> Objects.requireNonNull(modelCode).equals(it.getModel()))
.filter(it -> it.getModel().equals(modelCode))
.findFirst()
.ifPresent(selection -> {
templatePresentation.setIcon(Icons.OpenAI);
templatePresentation.setText(selection.getModel());
templatePresentation.setText(selection.getDisplayName());
});
break;
case ANTHROPIC:
@ -463,7 +463,7 @@ public class ModelComboBoxAction extends ComboBoxAction {
Icons.OpenAI,
comboBoxPresentation,
() -> ApplicationManager.getApplication().getService(ModelSettings.class)
.setModel(featureType, model.getName(), CUSTOM_OPENAI));
.setModel(featureType, model.getId(), CUSTOM_OPENAI));
}
private AnAction createGoogleModelAction(GoogleModel model, Presentation comboBoxPresentation) {

View file

@ -139,8 +139,7 @@ object CodeCompletionRequestFactory {
val activeService = service<CustomServicesSettings>()
.customServiceStateForFeatureType(FeatureType.CODE_COMPLETION)
val settings = activeService.codeCompletionSettings
val credential =
getCredential(CredentialKey.CustomServiceApiKey(activeService.name.orEmpty()))
val credential = getCredential(CredentialKey.CustomServiceApiKeyById(requireNotNull(activeService.id)))
return buildCustomRequest(
details,
settings.url!!,

View file

@ -3,7 +3,6 @@ package ee.carlrobert.codegpt.codecompletions
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import ee.carlrobert.codegpt.codecompletions.CodeCompletionRequestFactory.buildChatBasedFIMRequest
import ee.carlrobert.codegpt.codecompletions.CodeCompletionRequestFactory.buildChatBasedFIMHttpRequest
import ee.carlrobert.codegpt.codecompletions.CodeCompletionRequestFactory.buildCustomRequest
import ee.carlrobert.codegpt.codecompletions.CodeCompletionRequestFactory.buildLlamaRequest
@ -68,13 +67,13 @@ class CodeCompletionService(private val project: Project) {
}
CUSTOM_OPENAI -> {
val activeService = service<CustomServicesSettings>().state.active
val activeService =
service<CustomServicesSettings>().customServiceStateForFeatureType(FeatureType.CODE_COMPLETION)
val customSettings = activeService.codeCompletionSettings
val isChatBasedFIM = customSettings.infillTemplate == InfillPromptTemplate.CHAT_COMPLETION
val isChatBasedFIM =
customSettings.infillTemplate == InfillPromptTemplate.CHAT_COMPLETION
if (isChatBasedFIM) {
// Use chat completion endpoint for chat-based FIM with proper API key substitution
val credential = getCredential(CredentialKey.CustomServiceApiKey(activeService.name.orEmpty()))
val credential = getCredential(CredentialKey.CustomServiceApiKeyById(requireNotNull(activeService.id)))
createFactory(
CompletionClientProvider.getDefaultClientBuilder().build()
).newEventSource(
@ -88,7 +87,6 @@ class CodeCompletionService(private val project: Project) {
OpenAIChatCompletionEventSourceListener(eventListener)
)
} else {
// Use traditional completion endpoint
createFactory(
CompletionClientProvider.getDefaultClientBuilder().build()
).newEventSource(

View file

@ -33,7 +33,7 @@ class CustomOpenAIRequestFactory : BaseRequestFactory() {
params.psiStructure
),
true,
getCredential(CredentialKey.CustomServiceApiKey(service.name.orEmpty()))
getCredential(CredentialKey.CustomServiceApiKeyById(requireNotNull(service.id)))
)
return CustomOpenAIRequest(request)
}
@ -54,7 +54,7 @@ class CustomOpenAIRequestFactory : BaseRequestFactory() {
OpenAIChatCompletionStandardMessage("user", userPrompt)
),
stream,
getCredential(CredentialKey.CustomServiceApiKey(service.name.orEmpty()))
getCredential(CredentialKey.CustomServiceApiKeyById(requireNotNull(service.id)))
)
return CustomOpenAIRequest(request)
}
@ -67,7 +67,7 @@ class CustomOpenAIRequestFactory : BaseRequestFactory() {
service.chatCompletionSettings,
messages,
true,
getCredential(CredentialKey.CustomServiceApiKey(service.name.orEmpty()))
getCredential(CredentialKey.CustomServiceApiKeyById(requireNotNull(service.id)))
)
return CustomOpenAIRequest(request)
}

View file

@ -48,10 +48,15 @@ object CredentialsStore {
override val value: String = "OPENAI_API_KEY"
}
@Deprecated("Only for migration")
data class CustomServiceApiKey(val name: String) : CredentialKey() {
override val value: String = "CUSTOM_SERVICE_API_KEY:$name"
}
data class CustomServiceApiKeyById(val id: String) : CredentialKey() {
override val value: String = "CUSTOM_SERVICE_API_KEY_ID:$id"
}
@Deprecated("Only for migration")
data object CustomServiceApiKeyLegacy : CredentialKey() {
override val value: String = "CUSTOM_SERVICE_API_KEY"
@ -73,4 +78,4 @@ object CredentialsStore {
override val value: String = "MISTRAL_API_KEY"
}
}
}
}

View file

@ -1,7 +1,8 @@
package ee.carlrobert.codegpt.settings
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
import git4idea.GitUtil
import ee.carlrobert.codegpt.util.GitUtil
import git4idea.branch.GitBranchUtil
import java.time.LocalDate
@ -16,7 +17,10 @@ enum class Placeholder(val description: String, val code: String) {
"The complete source code contents of all files currently open in the IDE editor tabs, maintaining their formatting and structure.",
"$" + "OPEN_FILES"
),
ACTIVE_CONVERSATION("The complete conversation history with the AI assistant, including the most recent response and any relevant context from the current interaction.", "$" + "ACTIVE_CONVERSATION"),
ACTIVE_CONVERSATION(
"The complete conversation history with the AI assistant, including the most recent response and any relevant context from the current interaction.",
"$" + "ACTIVE_CONVERSATION"
),
PREFIX("Code before the cursor.", "$" + "PREFIX"),
SUFFIX("Code after the cursor.", "$" + "SUFFIX"),
FIM_PROMPT(
@ -36,15 +40,19 @@ class DatePlaceholderStrategy : PlaceholderStrategy {
}
class BranchNamePlaceholderStrategy(val project: Project) : PlaceholderStrategy {
private val logger = thisLogger()
override fun getReplacementValue(): String {
return try {
val repositories = GitUtil.getRepositoryManager(project).repositories
if (repositories.isEmpty() || repositories.size != 1) {
val repository = GitUtil.getProjectRepository(project)
if (repository == null) {
logger.error("Couldn't find repository for project")
return "BRANCH-UNKNOWN"
}
GitBranchUtil.getBranchNameOrRev(repositories[0])
} catch (ignore: Exception) {
GitBranchUtil.getBranchNameOrRev(repository)
} catch (ex: Exception) {
logger.error("Couldn't get git branch name replacement value", ex)
"BRANCH-UNKNOWN"
}
}

View file

@ -24,7 +24,7 @@ object LegacySettingsMigration {
return try {
val generalState = GeneralSettings.getCurrentState()
val selectedService = generalState.selectedService
if (selectedService != null) {
generalState.selectedService = null
createMigratedState(selectedService)
@ -40,7 +40,7 @@ object LegacySettingsMigration {
private fun createMigratedState(selectedService: ServiceType): ModelSettingsState {
return ModelSettingsState().apply {
val chatModel = getLegacyChatModelForService(selectedService)
setModelSelection(FeatureType.CHAT, chatModel, selectedService)
setModelSelection(FeatureType.AUTO_APPLY, chatModel, selectedService)
setModelSelection(FeatureType.COMMIT_MESSAGE, chatModel, selectedService)
@ -49,7 +49,7 @@ object LegacySettingsMigration {
val codeModel = getLegacyCodeModelForService(selectedService)
setModelSelection(FeatureType.CODE_COMPLETION, codeModel, selectedService)
if (selectedService == ServiceType.PROXYAI) {
setModelSelection(FeatureType.NEXT_EDIT, ModelRegistry.ZETA, ServiceType.PROXYAI)
} else {
@ -71,7 +71,8 @@ object LegacySettingsMigration {
}
ServiceType.ANTHROPIC -> {
AnthropicSettings.getCurrentState().model ?: ModelRegistry.CLAUDE_SONNET_4_20250514
AnthropicSettings.getCurrentState().model
?: ModelRegistry.CLAUDE_SONNET_4_20250514
}
ServiceType.GOOGLE -> {
@ -94,15 +95,10 @@ object LegacySettingsMigration {
}
ServiceType.CUSTOM_OPENAI -> {
val customServicesSettings = service<CustomServicesSettings>()
val services = customServicesSettings.state.services
val activeServiceName = customServicesSettings.state.active.name
if (!activeServiceName.isNullOrBlank()) {
activeServiceName
} else {
services.map { it.name }.lastOrNull()?.takeIf { it.isNotBlank() } ?: "Default"
}
service<CustomServicesSettings>().state.services
.map { it.name }
.lastOrNull()
?.takeIf { it.isNotBlank() } ?: "Default"
}
ServiceType.MISTRAL -> {
@ -111,7 +107,7 @@ object LegacySettingsMigration {
}
} catch (e: Exception) {
logger.warn("Failed to get legacy chat model for $serviceType", e)
getDefaultModelForService(serviceType)
throw e
}
}
@ -162,28 +158,4 @@ object LegacySettingsMigration {
null
}
}
private fun getDefaultModelForService(serviceType: ServiceType): String {
return when (serviceType) {
ServiceType.PROXYAI -> ModelRegistry.GEMINI_FLASH_2_5
ServiceType.OPENAI -> ModelRegistry.GPT_4O
ServiceType.ANTHROPIC -> ModelRegistry.CLAUDE_SONNET_4_20250514
ServiceType.GOOGLE -> ModelRegistry.GEMINI_2_0_FLASH
ServiceType.MISTRAL -> ModelRegistry.DEVSTRAL_MEDIUM_2507
ServiceType.OLLAMA -> ModelRegistry.LLAMA_3_2
ServiceType.LLAMA_CPP -> ModelRegistry.LLAMA_3_2_3B_INSTRUCT
ServiceType.CUSTOM_OPENAI -> {
// For Custom OpenAI, try to use the active service name if available
// If not available, use a placeholder that won't break model selection
try {
val customServicesSettings = service<CustomServicesSettings>()
val activeService = customServicesSettings.state.active
activeService?.name?.takeIf { it.isNotBlank() } ?: "Custom OpenAI"
} catch (e: Exception) {
logger.warn("Could not access CustomServicesSettings for default model, using placeholder", e)
"Custom OpenAI"
}
}
}
}
}

View file

@ -286,17 +286,15 @@ class ModelRegistry {
return try {
val customServicesSettings = service<CustomServicesSettings>()
customServicesSettings.state.services.mapNotNull { service ->
if (service.name.isNullOrBlank()) {
return@mapNotNull null
}
val serviceId = service.id ?: return@mapNotNull null
val serviceName = service.name ?: ""
val modelFromBody = service.codeCompletionSettings.body["model"]
val modelName = (modelFromBody as? String)
val displayName = if (!modelName.isNullOrEmpty()) {
if (modelName.length > 20) "$serviceName (...${modelName.takeLast(20)})" else "$serviceName ($modelName)"
} else serviceName
service.name?.let { serviceName ->
val modelFromBody = service.codeCompletionSettings.body["model"]
val modelName = (modelFromBody as? String) ?: "Unknown Model"
val displayName = "$serviceName ($modelName)"
ModelSelection(ServiceType.CUSTOM_OPENAI, serviceName, displayName)
}
ModelSelection(ServiceType.CUSTOM_OPENAI, serviceId, displayName)
}
} catch (e: Exception) {
logger.error("Failed to get Custom OpenAI code models", e)
@ -524,20 +522,14 @@ class ModelRegistry {
return try {
val customServicesSettings = service<CustomServicesSettings>()
customServicesSettings.state.services.mapNotNull { service ->
if (service.name.isNullOrBlank()) {
return@mapNotNull null
}
val serviceId = service.id ?: return@mapNotNull null
val serviceName = service.name ?: ""
val modelName = service.chatCompletionSettings.body["model"] as? String
val displayName = if (!modelName.isNullOrEmpty()) {
if (modelName.length > 20) "$serviceName (...${modelName.takeLast(20)})" else "$serviceName ($modelName)"
} else serviceName
service.name?.let { serviceName ->
val modelName = service.chatCompletionSettings.body["model"] as? String
val displayName = if (modelName != null) {
"$serviceName ($modelName)"
} else {
serviceName
}
ModelSelection(ServiceType.CUSTOM_OPENAI, serviceName, displayName)
}
ModelSelection(ServiceType.CUSTOM_OPENAI, serviceId, displayName)
}
} catch (e: Exception) {
logger.error("Failed to get Custom OpenAI models", e)

View file

@ -5,6 +5,7 @@ import com.intellij.openapi.components.*
import ee.carlrobert.codegpt.settings.service.FeatureType
import ee.carlrobert.codegpt.settings.service.ModelChangeNotifier
import ee.carlrobert.codegpt.settings.service.ServiceType
import ee.carlrobert.codegpt.settings.service.custom.CustomServicesSettings
@Service
@State(
@ -57,6 +58,7 @@ class ModelSettings : SimplePersistentStateComponent<ModelSettingsState>(ModelSe
val oldState = this.state
super.loadState(state)
migrateCustomOpenAIModelCodesToIds()
migrateMissingProviderInformation()
notifyIfChanged(oldState, this.state)
}
@ -74,29 +76,15 @@ class ModelSettings : SimplePersistentStateComponent<ModelSettingsState>(ModelSe
notifyModelChange(featureType, model, serviceType)
}
fun getModelSelection(featureType: FeatureType): ModelSelection {
fun getModelSelection(featureType: FeatureType): ModelSelection? {
val details = getModelDetailsState(featureType)
if (details == null) {
val defaultModel = ModelRegistry.getInstance().getDefaultModelForFeature(featureType)
state.setModelSelection(featureType, defaultModel.model, defaultModel.provider)
return defaultModel
}
return details.model?.let { model ->
return details?.model?.let { model ->
details.provider?.let { provider ->
ModelRegistry.getInstance().findModel(provider, model)
}
} ?: run {
val defaultModel = ModelRegistry.getInstance().getDefaultModelForFeature(featureType)
state.setModelSelection(featureType, defaultModel.model, defaultModel.provider)
defaultModel
}
}
fun getOrCreateModelSelection(featureType: FeatureType): ModelSelection {
return getModelSelection(featureType)
}
fun getModelForFeature(featureType: FeatureType): String? {
return getModelDetailsState(featureType)?.model
}
@ -136,19 +124,7 @@ class ModelSettings : SimplePersistentStateComponent<ModelSettingsState>(ModelSe
}
private fun findServiceTypeForModel(featureType: FeatureType, modelId: String?): ServiceType {
if (modelId == null) return ServiceType.PROXYAI
val provider = getProviderForFeature(featureType)
val models = getModelsForFeatureType(featureType)
if (provider != null) {
val modelWithProvider = models.find { it.model == modelId && it.provider == provider }
if (modelWithProvider != null) {
return modelWithProvider.provider
}
}
return models.find { it.model == modelId }?.provider ?: ServiceType.PROXYAI
return ServiceType.CUSTOM_OPENAI
}
private fun migrateMissingProviderInformation() {
@ -172,7 +148,33 @@ class ModelSettings : SimplePersistentStateComponent<ModelSettingsState>(ModelSe
return models.find { it.model == modelCode }?.provider
}
private fun migrateCustomOpenAIModelCodesToIds() {
val servicesByName: Map<String, List<String>> = try {
CustomServicesSettings::class.java
val settings = service<CustomServicesSettings>()
settings.state.services.groupBy({ it.name ?: "" }, { it.id ?: "" }).filterKeys { it.isNotEmpty() }
} catch (_: Exception) {
emptyMap()
}
if (servicesByName.isEmpty()) return
FeatureType.entries.forEach { featureType ->
val details = state.getModelSelection(featureType) ?: return@forEach
if (details.provider == ServiceType.CUSTOM_OPENAI && !details.model.isNullOrBlank()) {
val current = details.model!!
val ids = servicesByName[current]
if (ids != null && ids.size == 1) {
val id = ids.first()
if (id.isNotBlank() && id != current) {
state.setModelSelection(featureType, id, ServiceType.CUSTOM_OPENAI)
}
}
}
}
}
companion object {
fun getInstance(): ModelSettings = service()
}
}
}

View file

@ -17,7 +17,9 @@ class ModelSettingsState : BaseState() {
val registry = ModelRegistry.getInstance()
FeatureType.entries.forEach { featureType ->
val defaultModel = registry.getDefaultModelForFeature(featureType)
setModelSelection(featureType, defaultModel.model, defaultModel.provider)
if (defaultModel != null) {
setModelSelection(featureType, defaultModel.model, defaultModel.provider)
}
}
}

View file

@ -9,15 +9,13 @@ import ee.carlrobert.codegpt.settings.models.ModelRegistry
import ee.carlrobert.codegpt.settings.models.ModelSelection
import ee.carlrobert.codegpt.settings.models.ModelSettings
import ee.carlrobert.codegpt.settings.models.ModelSettingsForm
import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTAvailableModels
import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTModel
import ee.carlrobert.codegpt.util.ApplicationUtil
import ee.carlrobert.llm.client.codegpt.PricingPlan
import javax.swing.JComponent
class ModelReplacementDialog(
private val project: Project?,
serviceType: ServiceType
serviceType: ServiceType,
private val preferredCustomServiceId: String? = null
) : DialogWrapper(project) {
private val modelSettingsForm =
@ -67,19 +65,28 @@ class ModelReplacementDialog(
super.dispose()
}
private fun generateInitialModelSelections(serviceType: ServiceType): Map<FeatureType, ModelSelection?>? {
private fun generateInitialModelSelections(serviceType: ServiceType): Map<FeatureType, ModelSelection?> {
val registry = ModelRegistry.getInstance()
return when (serviceType) {
ServiceType.PROXYAI -> {
val userDetails = project?.let { CODEGPT_USER_DETAILS.get(it) }
FeatureType.entries.associateWith { featureType ->
return FeatureType.entries.associateWith { featureType ->
when (serviceType) {
ServiceType.PROXYAI -> {
val userDetails = project?.let { CODEGPT_USER_DETAILS.get(it) }
registry.getDefaultModelForFeature(featureType, userDetails?.pricingPlan)
}
}
else -> {
FeatureType.entries.associateWith { featureType ->
ServiceType.CUSTOM_OPENAI -> {
val models = registry.getAllModelsForFeature(featureType)
.filter { it.provider == serviceType }
if (!preferredCustomServiceId.isNullOrBlank()) {
models.firstOrNull { it.model == preferredCustomServiceId }
?: models.firstOrNull()
} else {
models.firstOrNull()
}
}
else -> {
registry.getAllModelsForFeature(featureType)
.firstOrNull { it.provider == serviceType }
}
@ -95,70 +102,59 @@ class ModelReplacementDialog(
}
companion object {
fun showDialog(serviceType: ServiceType): DialogResult {
val dialog = ModelReplacementDialog(ApplicationUtil.findCurrentProject(), serviceType)
fun showDialog(
serviceType: ServiceType,
preferredCustomServiceId: String? = null
): DialogResult {
val dialog = ModelReplacementDialog(
ApplicationUtil.findCurrentProject(),
serviceType,
preferredCustomServiceId
)
dialog.show()
return dialog.result
}
fun showDialogIfNeeded(serviceType: ServiceType): DialogResult {
return if (shouldShowDialog(serviceType)) {
showDialog(serviceType)
fun showDialogIfNeeded(
serviceType: ServiceType,
preferredCustomServiceId: String? = null
): DialogResult {
return if (shouldShowDialog(serviceType, preferredCustomServiceId)) {
showDialog(serviceType, preferredCustomServiceId)
} else {
DialogResult.KEEP_MODELS
}
}
private fun shouldShowDialog(serviceType: ServiceType): Boolean {
if (serviceType == ServiceType.PROXYAI) {
val project = ApplicationUtil.findCurrentProject()
val userDetails = project?.let { CODEGPT_USER_DETAILS.get(it) }
val modelSettings = service<ModelSettings>()
val registry = service<ModelRegistry>()
return FeatureType.entries.any { featureType ->
val currentSelection = modelSettings.getModelSelection(featureType)
val suggestedSelection =
registry.getDefaultModelForFeature(featureType, userDetails?.pricingPlan)
when {
currentSelection == null -> true
currentSelection.provider != serviceType -> true
currentSelection.model != suggestedSelection.model -> {
val suggestedModelAccessible =
CodeGPTAvailableModels.findByCode(suggestedSelection.model)?.let {
isModelAccessible(it, userDetails?.pricingPlan)
} == true
val currentModelNotAccessible =
CodeGPTAvailableModels.findByCode(currentSelection.model)?.let {
!isModelAccessible(it, userDetails?.pricingPlan)
} == true
suggestedModelAccessible || currentModelNotAccessible
}
else -> false
}
}
}
private fun shouldShowDialog(
serviceType: ServiceType,
preferredCustomServiceId: String? = null
): Boolean {
return FeatureType.entries.any { featureType ->
val registry = service<ModelRegistry>()
if (!registry.isFeatureSupportedByProvider(
featureType,
serviceType
)
) return@any false
val currentSelection = service<ModelSettings>().getModelSelection(featureType)
val availableModels = service<ModelRegistry>().getAllModelsForFeature(featureType)
val availableModels = registry.getAllModelsForFeature(featureType)
.filter { it.provider == serviceType }
if (availableModels.isEmpty()) return@any false
when {
currentSelection == null -> true
currentSelection.provider != serviceType -> true
availableModels.none { it.model == currentSelection.model } -> true
currentSelection?.provider != null && currentSelection.provider != serviceType -> true
serviceType == ServiceType.CUSTOM_OPENAI && !preferredCustomServiceId.isNullOrBlank() &&
currentSelection != null && currentSelection.model != preferredCustomServiceId -> true
currentSelection != null && availableModels.none { it.model == currentSelection.model } -> true
else -> false
}
}
}
private fun isModelAccessible(model: CodeGPTModel, userPricingPlan: PricingPlan?): Boolean {
if (userPricingPlan == null) return false
return userPricingPlan.ordinal >= model.pricingPlan.ordinal
}
}
}

View file

@ -59,6 +59,32 @@ class ModelSelectionService {
}
}
fun syncWithAvailableCustomOpenAIModels(preferredServiceId: String? = null) {
val registry = service<ModelRegistry>()
val settings = service<ModelSettings>()
FeatureType.entries.forEach { featureType ->
if (!registry.isFeatureSupportedByProvider(featureType, ServiceType.CUSTOM_OPENAI)) return@forEach
val current = settings.getModelSelection(featureType)
if (current?.provider != ServiceType.CUSTOM_OPENAI) return@forEach
val available = registry.getAllModelsForFeature(featureType)
.filter { it.provider == ServiceType.CUSTOM_OPENAI }
val isCurrentValid = available.any { it.model == current.model }
if (!isCurrentValid) {
val newId = when {
!preferredServiceId.isNullOrBlank() && available.any { it.model == preferredServiceId } -> preferredServiceId
available.isNotEmpty() -> available.first().model
else -> null
}
settings.setModelWithProvider(featureType, newId, ServiceType.CUSTOM_OPENAI)
}
}
}
companion object {
private val logger = thisLogger()
@ -68,4 +94,4 @@ class ModelSelectionService {
return ApplicationManager.getApplication().getService(ModelSelectionService::class.java)
}
}
}
}

View file

@ -2,9 +2,8 @@ package ee.carlrobert.codegpt.settings.service.custom
import com.intellij.openapi.components.service
import com.intellij.openapi.options.Configurable
import ee.carlrobert.codegpt.settings.service.ModelReplacementDialog
import ee.carlrobert.codegpt.settings.service.ServiceType
import ee.carlrobert.codegpt.settings.service.custom.form.CustomServiceListForm
import ee.carlrobert.codegpt.settings.service.ModelSelectionService
import ee.carlrobert.codegpt.settings.service.custom.form.CustomServiceForm
import ee.carlrobert.codegpt.util.coroutines.EdtDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
@ -14,14 +13,14 @@ import javax.swing.JComponent
class CustomServiceConfigurable : Configurable {
private val coroutineScope = CoroutineScope(SupervisorJob() + EdtDispatchers.Default)
private lateinit var component: CustomServiceListForm
private lateinit var component: CustomServiceForm
override fun getDisplayName(): String {
return "ProxyAI: Custom Service"
}
override fun createComponent(): JComponent {
component = CustomServiceListForm(service<CustomServicesSettings>(), coroutineScope)
component = CustomServiceForm(service<CustomServicesSettings>(), coroutineScope)
return component.getForm()
}
@ -29,8 +28,8 @@ class CustomServiceConfigurable : Configurable {
override fun apply() {
component.applyChanges()
ModelReplacementDialog.showDialogIfNeeded(ServiceType.CUSTOM_OPENAI)
ModelSelectionService.getInstance()
.syncWithAvailableCustomOpenAIModels(component.getSelectedServiceId())
}
override fun reset() {
@ -40,4 +39,4 @@ class CustomServiceConfigurable : Configurable {
override fun disposeUIResources() {
coroutineScope.cancel()
}
}
}

View file

@ -13,6 +13,7 @@ import ee.carlrobert.codegpt.settings.service.custom.template.CustomServiceChatC
import ee.carlrobert.codegpt.settings.service.custom.template.CustomServiceCodeCompletionTemplate
import ee.carlrobert.codegpt.settings.service.custom.template.CustomServiceTemplate
import ee.carlrobert.codegpt.util.BaseConverter
import java.util.UUID
import ee.carlrobert.codegpt.util.MapConverter
private const val DEFAULT_SERVICE_SETTINGS_NANE = "Default"
@ -63,13 +64,8 @@ class CustomServicesSettings :
val migrated = CustomServiceSettingsState().apply { copyFrom(oldSettingsService.state) }
state.services.clear()
state.services.add(migrated)
state.active = migrated
CredentialsStore.setCredential(CredentialsStore.CredentialKey.CustomServiceApiKeyLegacy, null)
CredentialsStore.setCredential(
CredentialsStore.CredentialKey.CustomServiceApiKey(state.active.name.orEmpty()),
oldApiKey
)
oldSettingsService.state.apply {
template = CustomServiceTemplate.OPENAI
@ -91,21 +87,49 @@ class CustomServicesSettings :
headers = mutableMapOf()
}
}
state.services.forEach { svc ->
if (svc.id.isNullOrBlank()) {
svc.id = UUID.randomUUID().toString()
}
}
runCatching {
val services = state.services.filter { !it.id.isNullOrBlank() }
val groups = services.groupBy { it.name ?: "" }
services.forEach { svc ->
val id = svc.id ?: return@forEach
val name = svc.name ?: return@forEach
val unique = name.isNotEmpty() && (groups[name]?.size == 1)
if (unique) {
val idKey = CredentialsStore.CredentialKey.CustomServiceApiKeyById(id)
val hasId = !CredentialsStore.getCredential(idKey).isNullOrEmpty()
if (!hasId) {
val legacy = CredentialsStore.getCredential(
CredentialsStore.CredentialKey.CustomServiceApiKey(name)
)
if (!legacy.isNullOrEmpty()) {
CredentialsStore.setCredential(idKey, legacy)
}
}
}
}
}
}
fun customServiceStateForFeatureType(featureType: FeatureType): CustomServiceSettingsState {
val modelSelection = service<ModelSelectionService>()
val featureSelection = modelSelection.getModelSelectionForFeature(featureType)
if (featureSelection.provider != ServiceType.CUSTOM_OPENAI)
if (featureSelection?.provider != ServiceType.CUSTOM_OPENAI)
throw IllegalStateException(
"Current selected ServiceType (${featureSelection}) is not of type 'CUSTOM_OPENAI'. " +
"This function should not be called in this context!"
)
return this.state.services
.find { it.name == featureSelection.model }
?: throw IllegalStateException("Unable to find custom service with name '${featureSelection.model}'.")
.find { it.id == featureSelection.model }
?: throw IllegalStateException("Unable to find custom service with id '${featureSelection.model}'.")
}
}
@ -120,8 +144,6 @@ class CustomServicesState(
@get:OptionTag(converter = CustomServiceSettingsListConverter::class)
var services by list<CustomServiceSettingsState>()
var active by property(initialState)
init {
services.add(initialState)
}
@ -129,6 +151,7 @@ class CustomServicesState(
@JsonIgnoreProperties(ignoreUnknown = true)
class CustomServiceSettingsState : BaseState() {
var id by string(UUID.randomUUID().toString())
var name by string(DEFAULT_SERVICE_SETTINGS_NANE)
var template by enum(CustomServiceTemplate.OPENAI)
var chatCompletionSettings by property(CustomServiceChatCompletionSettingsState())

View file

@ -1,7 +1,5 @@
package ee.carlrobert.codegpt.settings.service.custom.form
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.observable.util.whenTextChanged
import com.intellij.openapi.ui.MessageType
import com.intellij.util.ui.FormBuilder
import ee.carlrobert.codegpt.CodeGPTBundle
@ -31,7 +29,9 @@ class CustomServiceChatCompletionForm(
)
init {
testConnectionButton.addActionListener { testConnection() }
testConnectionButton.addActionListener {
testConnection()
}
}
var url: String
@ -73,42 +73,70 @@ class CustomServiceChatCompletionForm(
}
private fun testConnection() {
testConnectionButton.isEnabled = false
testConnectionButton.text = "Testing..."
val request = CustomOpenAIRequestFactory.buildCustomOpenAICompletionRequest(
"Test",
urlField.text,
tabbedPane.headers,
tabbedPane.body,
getApiKey.invoke()
)
CompletionRequestService.getInstance().getCustomOpenAIChatCompletionAsync(
CustomOpenAIRequestFactory.buildCustomOpenAICompletionRequest(
"Test",
urlField.text,
tabbedPane.headers,
tabbedPane.body,
getApiKey.invoke()
),
request,
TestConnectionEventListener()
)
}
internal inner class TestConnectionEventListener : CompletionEventListener<String?> {
private var responseReceived = false
override fun onMessage(value: String?, eventSource: EventSource) {
if (!value.isNullOrEmpty()) {
runInEdt {
OverlayUtil.showBalloon(
CodeGPTBundle.get("settingsConfigurable.service.custom.openai.connectionSuccess"),
MessageType.INFO,
testConnectionButton
)
eventSource.cancel()
}
if (!responseReceived) {
responseReceived = true
testConnectionButton.isEnabled = true
testConnectionButton.text =
CodeGPTBundle.get("settingsConfigurable.service.custom.openai.testConnection.label")
OverlayUtil.showBalloon(
"Connection successful!",
MessageType.INFO,
testConnectionButton
)
eventSource.cancel()
}
}
override fun onError(error: ErrorDetails, ex: Throwable) {
runInEdt {
testConnectionButton.isEnabled = true
testConnectionButton.text =
CodeGPTBundle.get("settingsConfigurable.service.custom.openai.testConnection.label")
OverlayUtil.showBalloon(
"Connection failed: ${error.message}",
MessageType.ERROR,
testConnectionButton
)
}
override fun onComplete(messageBuilder: StringBuilder) {
if (!responseReceived) {
testConnectionButton.isEnabled = true
testConnectionButton.text =
CodeGPTBundle.get("settingsConfigurable.service.custom.openai.testConnection.label")
OverlayUtil.showBalloon(
CodeGPTBundle.get("settingsConfigurable.service.custom.openai.connectionFailed")
+ "\n\n"
+ error.message,
MessageType.ERROR,
"Connection successful!",
MessageType.INFO,
testConnectionButton
)
}
}
override fun onCancelled(messageBuilder: StringBuilder) {
testConnectionButton.isEnabled = true
testConnectionButton.text =
CodeGPTBundle.get("settingsConfigurable.service.custom.openai.testConnection.label")
}
}
}

View file

@ -2,7 +2,6 @@ package ee.carlrobert.codegpt.settings.service.custom.form
import com.intellij.icons.AllIcons.General
import com.intellij.ide.HelpTooltip
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.MessageType
import com.intellij.openapi.ui.panel.ComponentPanelBuilder
@ -162,6 +161,9 @@ class CustomServiceCodeCompletionForm(
}
private fun testConnection() {
testConnectionButton.isEnabled = false
testConnectionButton.text = "Testing..."
val selectedTemplate = promptTemplateComboBox.selectedItem as InfillPromptTemplate
val testRequest = InfillRequest.Builder("Hello", "!", 0).build()
@ -193,31 +195,54 @@ class CustomServiceCodeCompletionForm(
}
}
internal inner class TestConnectionEventListener : CompletionEventListener<String?> {
private var responseReceived = false
override fun onMessage(value: String?, eventSource: EventSource) {
if (!value.isNullOrEmpty()) {
runInEdt {
OverlayUtil.showBalloon(
CodeGPTBundle.get("settingsConfigurable.service.custom.openai.connectionSuccess"),
MessageType.INFO,
testConnectionButton
)
eventSource.cancel()
}
if (!responseReceived) {
responseReceived = true
testConnectionButton.isEnabled = true
testConnectionButton.text =
CodeGPTBundle.get("settingsConfigurable.service.custom.openai.testConnection.label")
OverlayUtil.showBalloon(
"Connection successful!",
MessageType.INFO,
testConnectionButton
)
eventSource.cancel()
}
}
override fun onError(error: ErrorDetails, ex: Throwable) {
runInEdt {
testConnectionButton.isEnabled = true
testConnectionButton.text =
CodeGPTBundle.get("settingsConfigurable.service.custom.openai.testConnection.label")
OverlayUtil.showBalloon(
"Connection failed: ${error.message}",
MessageType.ERROR,
testConnectionButton
)
}
override fun onComplete(messageBuilder: StringBuilder) {
if (!responseReceived) {
testConnectionButton.isEnabled = true
testConnectionButton.text =
CodeGPTBundle.get("settingsConfigurable.service.custom.openai.testConnection.label")
OverlayUtil.showBalloon(
CodeGPTBundle.get("settingsConfigurable.service.custom.openai.connectionFailed")
+ "\n\n"
+ error.message,
MessageType.ERROR,
"Connection successful!",
MessageType.INFO,
testConnectionButton
)
}
}
override fun onCancelled(messageBuilder: StringBuilder) {
testConnectionButton.isEnabled = true
testConnectionButton.text =
CodeGPTBundle.get("settingsConfigurable.service.custom.openai.testConnection.label")
}
}
private fun updatePromptTemplateHelpTooltip(template: InfillPromptTemplate) {

View file

@ -31,22 +31,26 @@ import ee.carlrobert.codegpt.settings.service.custom.template.CustomServiceTempl
import ee.carlrobert.codegpt.ui.OverlayUtil
import ee.carlrobert.codegpt.ui.UIUtil
import ee.carlrobert.codegpt.util.ApplicationUtil
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import okhttp3.internal.toImmutableList
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.FlowLayout
import java.awt.event.ItemEvent
import java.net.MalformedURLException
import java.net.URL
import javax.swing.*
class CustomServiceListForm(
class CustomServiceForm(
private val service: CustomServicesSettings,
coroutineScope: CoroutineScope
private val coroutineScope: CoroutineScope
) {
private val formState = MutableStateFlow(service.state.mapToData())
@ -55,6 +59,9 @@ class CustomServiceListForm(
private val customSettingsFileProvider = CustomSettingsFileProvider()
private var lastSelectedIndex = 0
private var selectedServiceId: String? = null
private var pendingSelectedId: String? = null
private var suppressSelectionEvents: Boolean = false
private val customProvidersJBList = JBList(formState.value.services)
.apply {
@ -62,6 +69,7 @@ class CustomServiceListForm(
selectionMode = ListSelectionModel.SINGLE_SELECTION
addListSelectionListener { _ ->
if (suppressSelectionEvents) return@addListSelectionListener
val localSelectedIndex = selectedIndex
if (localSelectedIndex != -1) {
if (lastSelectedIndex != -1) {
@ -69,6 +77,7 @@ class CustomServiceListForm(
}
lastSelectedIndex = localSelectedIndex
selectedServiceId = model.getElementAt(localSelectedIndex).id
updateFormData(lastSelectedIndex)
}
}
@ -76,9 +85,36 @@ class CustomServiceListForm(
init {
formState
.onEach {
customProvidersJBList.setListData(it.services.toTypedArray())
customProvidersJBList.repaint()
.onEach { newState ->
val model = customProvidersJBList.model
val current = (0 until model.size).map { model.getElementAt(it) }
val currentIds = current.map { it.id }
val newIds = newState.services.map { it.id }
val idsChanged = currentIds != newIds
val namesChanged = !idsChanged && current.indices.any { i ->
i < newState.services.size && current[i].name != newState.services[i].name
}
if (idsChanged || namesChanged) {
SwingUtilities.invokeLater {
suppressSelectionEvents = true
try {
customProvidersJBList.setListData(newState.services.toTypedArray())
val targetId = pendingSelectedId ?: selectedServiceId
val idx = newState.services.indexOfFirst { it.id == targetId }
val targetIndex = if (idx >= 0) idx else 0
if (newState.services.isNotEmpty()) {
customProvidersJBList.selectedIndex = targetIndex
lastSelectedIndex = targetIndex
selectedServiceId = newState.services[targetIndex].id
updateFormDataSilently(newState.services[targetIndex])
}
pendingSelectedId = null
customProvidersJBList.repaint()
} finally {
suppressSelectionEvents = false
}
}
}
}
.launchIn(coroutineScope)
}
@ -99,10 +135,7 @@ class CustomServiceListForm(
init {
val selectedItem = formState.value.services.first()
apiKeyField.text = runBlocking(Dispatchers.IO) {
getCredential(CredentialKey.CustomServiceApiKey(selectedItem.name.orEmpty()))
}
apiKeyField.text = getCredential(CredentialKey.CustomServiceApiKeyById(selectedItem.id))
chatCompletionsForm =
CustomServiceChatCompletionForm(selectedItem.chatCompletionSettings, this::getApiKey)
codeCompletionsForm =
@ -110,31 +143,8 @@ class CustomServiceListForm(
tabbedPane = JBTabbedPane().apply {
add(CodeGPTBundle.get("shared.chatCompletions"), chatCompletionsForm.form)
add(CodeGPTBundle.get("shared.codeCompletions"), codeCompletionsForm.form)
templateComboBox.selectedItem = selectedItem.template
}
nameField.text = selectedItem.name
templateComboBox.addItemListener {
val template = it.item as CustomServiceTemplate
updateTemplateHelpTextTooltip(template)
chatCompletionsForm.run {
url = template.chatCompletionTemplate.url
headers = template.chatCompletionTemplate.headers
body = template.chatCompletionTemplate.body
}
if (template.codeCompletionTemplate != null) {
codeCompletionsForm.run {
url = template.codeCompletionTemplate.url
headers = template.codeCompletionTemplate.headers
body = template.codeCompletionTemplate.body
parseResponseAsChatCompletions =
template.codeCompletionTemplate.parseResponseAsChatCompletions
}
tabbedPane.setEnabledAt(1, true)
} else {
tabbedPane.selectedIndex = 0
tabbedPane.setEnabledAt(1, false)
}
}
exportButton =
JButton(CodeGPTBundle.get("settingsConfigurable.service.custom.openai.exportSettings")).apply {
addActionListener { exportSettingsToFile() }
@ -143,34 +153,83 @@ class CustomServiceListForm(
JButton(CodeGPTBundle.get("settingsConfigurable.service.custom.openai.importSettings")).apply {
addActionListener { importSettingsFromFile() }
}
updateTemplateHelpTextTooltip(selectedItem.template)
templateComboBox.addItemListener { event ->
if (event.stateChange == ItemEvent.SELECTED) {
val template = event.item as CustomServiceTemplate
updateTemplateHelpTextTooltip(template)
chatCompletionsForm.run {
url = template.chatCompletionTemplate.url
headers = template.chatCompletionTemplate.headers
body = template.chatCompletionTemplate.body
}
if (template.codeCompletionTemplate != null) {
codeCompletionsForm.run {
url = template.codeCompletionTemplate.url
headers = template.codeCompletionTemplate.headers
body = template.codeCompletionTemplate.body
parseResponseAsChatCompletions =
template.codeCompletionTemplate.parseResponseAsChatCompletions
}
tabbedPane.setEnabledAt(1, true)
} else {
tabbedPane.selectedIndex = 0
tabbedPane.setEnabledAt(1, false)
}
}
}
updateFormDataSilently(selectedItem)
SwingUtilities.invokeLater {
if (customProvidersJBList.model.size > 0) {
customProvidersJBList.selectedIndex = 0
lastSelectedIndex = 0
selectedServiceId = customProvidersJBList.model.getElementAt(0).id
}
}
}
private fun updateFormData(index: Int) {
val selectedItem = formState.value.services[index]
SwingUtilities.invokeLater {
updateFormDataSilently(selectedItem)
}
}
chatCompletionsForm.apply {
val chatCompletionSettings = selectedItem.chatCompletionSettings
url = chatCompletionSettings.url.orEmpty()
body = chatCompletionSettings.body.toMutableMap()
headers = chatCompletionSettings.headers.toMutableMap()
private fun updateFormDataSilently(selectedItem: CustomServiceSettingsData) {
val templateListener = templateComboBox.itemListeners.firstOrNull()
templateListener?.let { templateComboBox.removeItemListener(it) }
try {
chatCompletionsForm.apply {
val chatCompletionSettings = selectedItem.chatCompletionSettings
url = chatCompletionSettings.url.orEmpty()
body = chatCompletionSettings.body.toMutableMap()
headers = chatCompletionSettings.headers.toMutableMap()
}
codeCompletionsForm.apply {
val codeCompletionSettings = selectedItem.codeCompletionSettings
url = codeCompletionSettings.url.orEmpty()
body = codeCompletionSettings.body.toMutableMap()
headers = codeCompletionSettings.headers.toMutableMap()
infillTemplate = codeCompletionSettings.infillTemplate
codeCompletionsEnabled = codeCompletionSettings.codeCompletionsEnabled
parseResponseAsChatCompletions =
codeCompletionSettings.parseResponseAsChatCompletions
}
apiKeyField.text = getCredential(CredentialKey.CustomServiceApiKeyById(selectedItem.id))
nameField.text = selectedItem.name
templateComboBox.selectedItem = selectedItem.template
updateTemplateHelpTextTooltip(selectedItem.template)
} finally {
templateListener?.let { templateComboBox.addItemListener(it) }
}
codeCompletionsForm.apply {
val codeCompletionSettings = selectedItem.codeCompletionSettings
url = codeCompletionSettings.url.orEmpty()
body = codeCompletionSettings.body.toMutableMap()
headers = codeCompletionSettings.headers.toMutableMap()
infillTemplate = codeCompletionSettings.infillTemplate
codeCompletionsEnabled = codeCompletionSettings.codeCompletionsEnabled
parseResponseAsChatCompletions = codeCompletionSettings.parseResponseAsChatCompletions
}
apiKeyField.text = selectedItem.apiKey
nameField.text = selectedItem.name
templateComboBox.selectedItem = selectedItem.template
updateTemplateHelpTextTooltip(selectedItem.template)
}
private fun updateStateFromForm(editedIndex: Int) {
if (editedIndex < 0 || editedIndex >= formState.value.services.size) return
formState.update { state ->
val editedItem = state.services[editedIndex]
@ -252,41 +311,48 @@ class CustomServiceListForm(
private fun handleRemoveAction() {
val prevSelectedIndex = customProvidersJBList.selectedIndex
// Update form state before deletion to ensure current edits are saved
if (lastSelectedIndex != -1 && lastSelectedIndex < formState.value.services.size) {
updateStateFromForm(lastSelectedIndex)
}
val current = formState.value.services
val targetNeighborId = when {
current.isEmpty() -> null
prevSelectedIndex <= 0 && current.size >= 2 -> current[1].id
prevSelectedIndex > 0 -> current[prevSelectedIndex - 1].id
else -> null
}
formState.update { state ->
state.copy(services = state.services.filterIndexed { index, _ ->
index != customProvidersJBList.selectedIndex
})
}
val newSelectedIndex = if (prevSelectedIndex == 0) {
0
} else {
prevSelectedIndex - 1
state.copy(services = state.services.filterIndexed { index, _ -> index != prevSelectedIndex })
}
pendingSelectedId = targetNeighborId
lastSelectedIndex = -1
updateFormData(newSelectedIndex)
customProvidersJBList.selectedIndex = newSelectedIndex
}
private fun handleDuplicateAction() {
formState.update {
val selectedIndex = customProvidersJBList.selectedIndex
val copiedService =
it.services[selectedIndex].copy(name = it.services[selectedIndex].name + "Copied")
val src = it.services[selectedIndex]
val copiedService = src.copy(
id = java.util.UUID.randomUUID().toString(),
name = src.name + "Copied"
)
it.copy(
services = it.services + copiedService
)
}
customProvidersJBList.selectedIndex = formState.value.services.lastIndex
pendingSelectedId = formState.value.services.last().id
}
private fun handleAddAction() {
formState.update {
it.copy(
services = it.services + CustomServiceSettingsState().apply { name += it.services.size }
.mapToData()
)
}
customProvidersJBList.selectedIndex = formState.value.services.lastIndex
val newData = CustomServiceSettingsState().apply { name += formState.value.services.size }
.mapToData()
formState.update { it.copy(services = it.services + newData) }
pendingSelectedId = newData.id
}
private fun createContentPanel(): JPanel = FormBuilder.createFormBuilder()
@ -317,7 +383,9 @@ class CustomServiceListForm(
fun getApiKey() = String(apiKeyField.password).ifEmpty { null }
fun isModified(): Boolean {
updateStateFromForm(lastSelectedIndex)
if (lastSelectedIndex >= 0 && lastSelectedIndex < formState.value.services.size) {
updateStateFromForm(lastSelectedIndex)
}
return service.state.mapToData() != formState.value
}
@ -376,12 +444,8 @@ class CustomServiceListForm(
.inSmartMode(it)
.finishOnUiThread(ModalityState.defaultModalityState()) { settings ->
if (settings != null) {
val newActualService =
settings.firstOrNull { it.name == formState.value.active.name }
?: settings.first()
formState.update { state ->
state.copy(services = settings, active = newActualService)
state.copy(services = settings)
}
updateFormData(0)
}
@ -432,51 +496,41 @@ class CustomServiceListForm(
}
fun applyChanges() {
if (!validateServiceNames()) {
OverlayUtil.showBalloon(
"Service names must be unique",
MessageType.ERROR,
customProvidersJBList,
)
return
if (lastSelectedIndex != -1 && lastSelectedIndex < formState.value.services.size) {
updateStateFromForm(lastSelectedIndex)
}
val formStateValue = formState.value
val newActualService =
formStateValue.services.firstOrNull { it.name == formStateValue.active.name }
?: formStateValue.services.first()
// Cleanup saved api keys
val savedServicesName = service.state.services.mapNotNull { it.name }
val deletedServices =
savedServicesName.subtract(formStateValue.services.mapNotNull { it.name }.toSet())
deletedServices.forEach { deletedServiceName ->
CredentialsStore.setCredential(
CredentialKey.CustomServiceApiKey(deletedServiceName),
null
)
val prevById = service.state.services.associateBy { it.id }
val savedIds = prevById.keys.filterNotNull().toSet()
val newIds = formStateValue.services.map { it.id }.toSet()
val deletedIds = savedIds.subtract(newIds)
deletedIds.forEach { deletedId ->
CredentialsStore.setCredential(CredentialKey.CustomServiceApiKeyById(deletedId), null)
}
// Save apiKeys
formStateValue.services.forEach {
CredentialsStore.setCredential(
CredentialKey.CustomServiceApiKey(it.name.orEmpty()),
it.apiKey
)
if (it.id.isNotBlank()) {
CredentialsStore.setCredential(
CredentialKey.CustomServiceApiKeyById(it.id),
it.apiKey
)
}
}
// Save settings
service.state.run {
services = formStateValue.services.mapTo(mutableListOf()) { it.mapToState() }
active = newActualService.mapToState()
}
formState.value = service.state.mapToData()
}
private fun validateServiceNames(): Boolean {
val serviceNames = formState.value.services.mapNotNull { it.name }
val uniqueNames = serviceNames.toSet()
return serviceNames.size == uniqueNames.size
fun getSelectedServiceId(): String? {
val idx = customProvidersJBList.selectedIndex
return if (idx >= 0 && idx < formState.value.services.size) {
formState.value.services[idx].id
} else {
selectedServiceId
}
}
fun resetForm() {
@ -503,4 +557,4 @@ class CustomServiceListForm(
throw RuntimeException(e)
}
}
}
}

View file

@ -1,14 +1,12 @@
package ee.carlrobert.codegpt.settings.service.custom.form
import com.intellij.ui.render.LabelBasedRenderer
import ee.carlrobert.codegpt.settings.service.custom.form.model.CustomServiceSettingsData
import java.awt.Component
import javax.swing.JLabel
import javax.swing.JList
import javax.swing.ListCellRenderer
internal class CustomServiceNameListRenderer : LabelBasedRenderer(), ListCellRenderer<CustomServiceSettingsData> {
private val delegate = List<CustomServiceSettingsData>()
internal class CustomServiceNameListRenderer : JLabel(), ListCellRenderer<CustomServiceSettingsData> {
override fun getListCellRendererComponent(
list: JList<out CustomServiceSettingsData>,
@ -16,9 +14,12 @@ internal class CustomServiceNameListRenderer : LabelBasedRenderer(), ListCellRen
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component =
delegate.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus)
.also { component ->
(component as? JLabel)?.text = value?.name ?: ""
}
}
): Component {
text = value?.name ?: ""
isOpaque = true
background = if (isSelected) list.selectionBackground else list.background
foreground = if (isSelected) list.selectionForeground else list.foreground
font = list.font
return this
}
}

View file

@ -1,8 +1,10 @@
package ee.carlrobert.codegpt.settings.service.custom.form.model
import ee.carlrobert.codegpt.settings.service.custom.template.CustomServiceTemplate
import java.util.UUID
data class CustomServiceSettingsData(
val id: String = UUID.randomUUID().toString(),
val name: String?,
val template: CustomServiceTemplate,
val apiKey: String?,

View file

@ -1,6 +1,3 @@
package ee.carlrobert.codegpt.settings.service.custom.form.model
data class CustomServicesStateData(
val services: List<CustomServiceSettingsData>,
val active: CustomServiceSettingsData
)
data class CustomServicesStateData(val services: List<CustomServiceSettingsData>)

View file

@ -3,10 +3,12 @@ package ee.carlrobert.codegpt.settings.service.custom.form.model
import ee.carlrobert.codegpt.settings.service.custom.CustomServiceChatCompletionSettingsState
import ee.carlrobert.codegpt.settings.service.custom.CustomServiceCodeCompletionSettingsState
import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettingsState
import java.util.UUID
fun CustomServiceSettingsData.mapToState(): CustomServiceSettingsState =
CustomServiceSettingsState().also { serviceState ->
serviceState.id = if (id.isBlank()) UUID.randomUUID().toString() else id
serviceState.name = name
serviceState.template = template
serviceState.chatCompletionSettings = chatCompletionSettings.mapToState()

View file

@ -5,25 +5,26 @@ import ee.carlrobert.codegpt.settings.service.custom.CustomServiceChatCompletion
import ee.carlrobert.codegpt.settings.service.custom.CustomServiceCodeCompletionSettingsState
import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettingsState
import ee.carlrobert.codegpt.settings.service.custom.CustomServicesState
import java.util.UUID
fun CustomServicesState.mapToData(): CustomServicesStateData =
CustomServicesStateData(
services = services.map { it.mapToData() },
active = active.mapToData()
)
CustomServicesStateData(services.map { it.mapToData() })
fun CustomServiceSettingsState.mapToData(): CustomServiceSettingsData =
CustomServiceSettingsData(
id = id ?: UUID.randomUUID().toString(),
name = name,
template = template,
apiKey = CredentialsStore.getCredential(CredentialsStore.CredentialKey.CustomServiceApiKey(name.orEmpty())),
apiKey = if (!id.isNullOrEmpty())
CredentialsStore.getCredential(CredentialsStore.CredentialKey.CustomServiceApiKeyById(id!!))
else null,
chatCompletionSettings = chatCompletionSettings.mapToData(),
codeCompletionSettings = codeCompletionSettings.mapToData()
)
fun CustomServiceChatCompletionSettingsState.mapToData(): CustomServiceChatCompletionSettingsData =
CustomServiceChatCompletionSettingsData(
url = url,
url = url ?: "",
headers = headers,
body = body
)
@ -33,7 +34,7 @@ fun CustomServiceCodeCompletionSettingsState.mapToData(): CustomServiceCodeCompl
codeCompletionsEnabled = codeCompletionsEnabled,
parseResponseAsChatCompletions = parseResponseAsChatCompletions,
infillTemplate = infillTemplate,
url = url,
url = url ?: "",
headers = headers,
body = body
)
)

View file

@ -26,8 +26,13 @@ object GitUtil {
@JvmStatic
fun getProjectRepository(project: Project): GitRepository? {
val repositoryManager = project.service<GitRepositoryManager>()
return repositoryManager.getRepositoryForFile(project.guessProjectDir())
?: repositoryManager.repositories.firstOrNull()
return try {
repositoryManager.getRepositoryForFile(project.guessProjectDir())
?: repositoryManager.repositories.firstOrNull()
} catch (e: Exception) {
logger.warn("Failed to get git repository", e)
repositoryManager.repositories.firstOrNull()
}
}
fun getCurrentChanges(project: Project): String? {

View file

@ -173,23 +173,14 @@ class ModelSettingsTest : IntegrationTest() {
assertThat(notification.serviceType).isEqualTo(ServiceType.ANTHROPIC)
}
fun `test getOrCreateModelSelection with existing selection returns stored model`() {
modelSettings.setModelWithProvider(FeatureType.CHAT, "gpt-4o", ServiceType.OPENAI)
val result = modelSettings.getOrCreateModelSelection(FeatureType.CHAT)
assertThat(result.provider).isEqualTo(ServiceType.OPENAI)
assertThat(result.model).isEqualTo("gpt-4o")
}
fun `test getModelSelection with valid feature returns model selection`() {
modelSettings.setModelWithProvider(FeatureType.CHAT, "gpt-4o", ServiceType.OPENAI)
val result = modelSettings.getModelSelection(FeatureType.CHAT)
assertThat(result).isNotNull
assertThat(result.provider).isEqualTo(ServiceType.OPENAI)
assertThat(result.model).isEqualTo("gpt-4o")
assertThat(result?.provider).isEqualTo(ServiceType.OPENAI)
assertThat(result?.model).isEqualTo("gpt-4o")
}
fun `test getModelForFeature returns stored model`() {