diff --git a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettings.java b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettings.java index 8e3368cf1..ac70ca4a2 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettings.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettings.java @@ -1,10 +1,13 @@ package ee.carlrobert.codegpt.settings; +import static ee.carlrobert.codegpt.settings.service.ModelRole.CHAT_ROLE; + import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.PersistentStateComponent; import com.intellij.openapi.components.State; import com.intellij.openapi.components.Storage; import ee.carlrobert.codegpt.completions.llama.LlamaModel; +import ee.carlrobert.codegpt.settings.service.ModelRole; import ee.carlrobert.codegpt.settings.service.ServiceType; import ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettings; import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings; @@ -39,7 +42,11 @@ public static GeneralSettings getInstance() { } public static ServiceType getSelectedService() { - return getCurrentState().getSelectedService(); + return getCurrentState().getSelectedService(CHAT_ROLE); + } + + public static ServiceType getSelectedService(ModelRole role) { + return getCurrentState().getSelectedService(role); } public static boolean isSelected(ServiceType serviceType) { diff --git a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsState.java b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsState.java index c2c75f69f..267da6e2e 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsState.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsState.java @@ -1,6 +1,10 @@ package ee.carlrobert.codegpt.settings; +import static ee.carlrobert.codegpt.settings.service.ModelRole.CHAT_ROLE; +import static ee.carlrobert.codegpt.settings.service.ModelRole.CODECOMPLETION_ROLE; + import com.intellij.openapi.application.ApplicationManager; +import ee.carlrobert.codegpt.settings.service.ModelRole; import ee.carlrobert.codegpt.settings.service.ProviderChangeNotifier; import ee.carlrobert.codegpt.settings.service.ServiceType; @@ -9,6 +13,7 @@ public class GeneralSettingsState { private String displayName = ""; private String avatarBase64 = ""; private ServiceType selectedService = ServiceType.CODEGPT; + private ServiceType codeCompletionService = ServiceType.CODEGPT; public String getDisplayName() { if (displayName == null || displayName.isEmpty()) { @@ -33,16 +38,52 @@ public void setAvatarBase64(String avatarBase64) { this.avatarBase64 = avatarBase64; } + public ServiceType getSelectedService(ModelRole role) { + switch (role) { + case CHAT_ROLE -> { + return selectedService; + } + case CODECOMPLETION_ROLE -> { + return codeCompletionService; + } + default -> { + throw new AssertionError(); + } + } + } + public ServiceType getSelectedService() { - return selectedService; + return getSelectedService(CHAT_ROLE); + } + + public ServiceType getSelectedCodeCompletionService() { + return getSelectedService(CODECOMPLETION_ROLE); + } + + public void setSelectedService(ModelRole role, ServiceType selectedService) { + switch (role) { + case CHAT_ROLE -> { + this.selectedService = selectedService; + + ApplicationManager.getApplication() + .getMessageBus() + .syncPublisher(ProviderChangeNotifier.getTOPIC()) + .providerChanged(selectedService); + } + case CODECOMPLETION_ROLE -> { + this.codeCompletionService = selectedService; + } + default -> { + throw new AssertionError(); + } + } } public void setSelectedService(ServiceType selectedService) { - this.selectedService = selectedService; + setSelectedService(CHAT_ROLE, selectedService); + } - ApplicationManager.getApplication() - .getMessageBus() - .syncPublisher(ProviderChangeNotifier.getTOPIC()) - .providerChanged(selectedService); + public void setSelectedCodeCompletionService(ServiceType selectedService) { + setSelectedService(CODECOMPLETION_ROLE, selectedService); } } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/ModelRole.java b/src/main/java/ee/carlrobert/codegpt/settings/service/ModelRole.java new file mode 100644 index 000000000..ff0403722 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/ModelRole.java @@ -0,0 +1,6 @@ +package ee.carlrobert.codegpt.settings.service; + +public enum ModelRole { + CHAT_ROLE, + CODECOMPLETION_ROLE +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt index 034708047..9329d778d 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt @@ -6,6 +6,7 @@ import com.intellij.openapi.components.service import com.intellij.openapi.project.DumbAwareAction import ee.carlrobert.codegpt.codecompletions.CodeCompletionService import ee.carlrobert.codegpt.settings.GeneralSettings +import ee.carlrobert.codegpt.settings.service.ModelRole.CODECOMPLETION_ROLE import ee.carlrobert.codegpt.settings.service.ServiceType.* import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings import ee.carlrobert.codegpt.settings.service.custom.CustomServicesSettings @@ -17,7 +18,7 @@ abstract class CodeCompletionFeatureToggleActions( private val enableFeatureAction: Boolean ) : DumbAwareAction() { - override fun actionPerformed(e: AnActionEvent) = when (GeneralSettings.getSelectedService()) { + override fun actionPerformed(e: AnActionEvent) = when (GeneralSettings.getSelectedService(CODECOMPLETION_ROLE)) { CODEGPT -> service().state.codeCompletionSettings::codeCompletionsEnabled::set OPENAI -> OpenAISettings.getCurrentState()::setCodeCompletionsEnabled @@ -34,7 +35,7 @@ abstract class CodeCompletionFeatureToggleActions( }(enableFeatureAction) override fun update(e: AnActionEvent) { - val selectedService = GeneralSettings.getSelectedService() + val selectedService = GeneralSettings.getSelectedService(CODECOMPLETION_ROLE) val codeCompletionEnabled = e.project?.service()?.isCodeCompletionsEnabled(selectedService) ?: false diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionEventListener.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionEventListener.kt index f4f139e2d..c84133678 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionEventListener.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionEventListener.kt @@ -13,6 +13,7 @@ import ee.carlrobert.codegpt.CodeGPTKeys import ee.carlrobert.codegpt.codecompletions.edit.GrpcClientService import ee.carlrobert.codegpt.settings.GeneralSettings import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings +import ee.carlrobert.codegpt.settings.service.ModelRole.CODECOMPLETION_ROLE import ee.carlrobert.codegpt.settings.service.ServiceType import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings import ee.carlrobert.codegpt.treesitter.CodeCompletionParserFactory @@ -156,7 +157,7 @@ class CodeCompletionEventListener( } override fun onError(error: ErrorDetails, ex: Throwable) { - val isCodeGPTService = GeneralSettings.getSelectedService() == ServiceType.CODEGPT + val isCodeGPTService = GeneralSettings.getSelectedService(CODECOMPLETION_ROLE) == ServiceType.CODEGPT if (isCodeGPTService && "RATE_LIMIT_EXCEEDED" == error.code) { service().state .codeCompletionSettings diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt index f11f6e448..0bef67b76 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt @@ -127,7 +127,7 @@ object CodeCompletionRequestFactory { } return OllamaCompletionRequest.Builder( - settings.model, + settings.codeCompletionModel, prompt ) .setSuffix(if (settings.fimOverride) null else details.suffix) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionService.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionService.kt index 68441f036..8e7257327 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionService.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionService.kt @@ -12,6 +12,7 @@ import ee.carlrobert.codegpt.codecompletions.edit.GrpcClientService import ee.carlrobert.codegpt.completions.CompletionClientProvider import ee.carlrobert.codegpt.completions.llama.LlamaModel import ee.carlrobert.codegpt.settings.GeneralSettings +import ee.carlrobert.codegpt.settings.service.ModelRole.CODECOMPLETION_ROLE import ee.carlrobert.codegpt.settings.service.ServiceType import ee.carlrobert.codegpt.settings.service.ServiceType.* import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings @@ -41,12 +42,12 @@ class CodeCompletionService(private val project: Project) { .getOrDefault("model", null) as String LLAMA_CPP -> LlamaModel.findByHuggingFaceModel(LlamaSettings.getCurrentState().huggingFaceModel).label - OLLAMA -> service().state.model + OLLAMA -> service().state.codeCompletionModel else -> null } } - fun isCodeCompletionsEnabled(): Boolean = isCodeCompletionsEnabled(GeneralSettings.getSelectedService()) + fun isCodeCompletionsEnabled(): Boolean = isCodeCompletionsEnabled(GeneralSettings.getSelectedService(CODECOMPLETION_ROLE)) fun isCodeCompletionsEnabled(selectedService: ServiceType): Boolean = when (selectedService) { @@ -62,7 +63,7 @@ class CodeCompletionService(private val project: Project) { infillRequest: InfillRequest, eventListener: CompletionEventListener ): EventSource { - return when (val selectedService = GeneralSettings.getSelectedService()) { + return when (val selectedService = GeneralSettings.getSelectedService(CODECOMPLETION_ROLE)) { OPENAI -> CompletionClientProvider.getOpenAIClient() .getCompletionAsync(buildOpenAIRequest(infillRequest), eventListener) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/DebouncedCodeCompletionProvider.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/DebouncedCodeCompletionProvider.kt index 67cee0198..4d3ae3701 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/DebouncedCodeCompletionProvider.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/DebouncedCodeCompletionProvider.kt @@ -10,6 +10,7 @@ import com.intellij.platform.workspace.storage.impl.cache.cache import ee.carlrobert.codegpt.CodeGPTKeys.REMAINING_CODE_COMPLETION import ee.carlrobert.codegpt.codecompletions.edit.GrpcClientService import ee.carlrobert.codegpt.settings.GeneralSettings +import ee.carlrobert.codegpt.settings.service.ModelRole.* import ee.carlrobert.codegpt.settings.service.ServiceType import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings import ee.carlrobert.codegpt.settings.service.custom.CustomServicesSettings @@ -74,7 +75,7 @@ class DebouncedCodeCompletionProvider : DebouncedInlineCompletionProvider() { var eventListener = CodeCompletionEventListener(request.editor, this) - if (GeneralSettings.getSelectedService() == ServiceType.CODEGPT) { + if (GeneralSettings.getSelectedService(CODECOMPLETION_ROLE) == ServiceType.CODEGPT) { project.service().getCodeCompletionAsync(eventListener, request, this) return@channelFlow } @@ -103,7 +104,7 @@ class DebouncedCodeCompletionProvider : DebouncedInlineCompletionProvider() { } override fun isEnabled(event: InlineCompletionEvent): Boolean { - val selectedService = GeneralSettings.getSelectedService() + val selectedService = GeneralSettings.getSelectedService(CODECOMPLETION_ROLE) val codeCompletionsEnabled = when (selectedService) { ServiceType.CODEGPT -> service().state.codeCompletionSettings.codeCompletionsEnabled ServiceType.OPENAI -> OpenAISettings.getCurrentState().isCodeCompletionsEnabled diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ServiceConfigurable.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ServiceConfigurable.kt index c3352e148..f12c29178 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ServiceConfigurable.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ServiceConfigurable.kt @@ -1,5 +1,6 @@ package ee.carlrobert.codegpt.settings.service +import ee.carlrobert.codegpt.settings.service.ModelRole.* import com.intellij.openapi.components.service import com.intellij.openapi.options.Configurable import ee.carlrobert.codegpt.conversations.ConversationsState @@ -23,24 +24,28 @@ class ServiceConfigurable : Configurable { } override fun isModified(): Boolean { - return component.getSelectedService() != service().state.selectedService + return component.getSelectedService(CHAT_ROLE) != service().state.getSelectedService(CHAT_ROLE) + || component.getSelectedService(CODECOMPLETION_ROLE) != service().state.getSelectedService(CODECOMPLETION_ROLE) } override fun apply() { val state = service().state - state.selectedService = component.getSelectedService() + state.setSelectedService(CHAT_ROLE, component.getSelectedService(CHAT_ROLE)) - val serviceChanged = component.getSelectedService() != state.selectedService + val serviceChanged = component.getSelectedService(CHAT_ROLE) != state.selectedService if (serviceChanged) { resetActiveTab() TelemetryAction.SETTINGS_CHANGED.createActionMessage() - .property("service", component.getSelectedService().code.lowercase()) + .property("service", component.getSelectedService(CHAT_ROLE).code.lowercase()) .send() } + + state.setSelectedService(CODECOMPLETION_ROLE, component.getSelectedService(CODECOMPLETION_ROLE)) } override fun reset() { - component.setSelectedService(service().state.selectedService) + component.setSelectedService(CHAT_ROLE,service().state.getSelectedService(CHAT_ROLE)) + component.setSelectedService(CODECOMPLETION_ROLE,service().state.getSelectedService(CODECOMPLETION_ROLE)) } private fun resetActiveTab() { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ServiceConfigurableComponent.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ServiceConfigurableComponent.kt index 08249e9bd..5c2c5ded9 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ServiceConfigurableComponent.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ServiceConfigurableComponent.kt @@ -1,5 +1,6 @@ package ee.carlrobert.codegpt.settings.service +import ee.carlrobert.codegpt.settings.service.ModelRole.* import com.intellij.ide.DataManager import com.intellij.openapi.components.service import com.intellij.openapi.options.ex.Settings @@ -23,15 +24,25 @@ class ServiceConfigurableComponent { private var serviceComboBox: ComboBox = ComboBox(EnumComboBoxModel(ServiceType::class.java)).apply { - selectedItem = service().state.selectedService + selectedItem = service().state.getSelectedService(CHAT_ROLE) + } + private var codeCompletionServiceComboBox: ComboBox = + ComboBox(EnumComboBoxModel(ServiceType::class.java)).apply { + selectedItem = service().state.getSelectedService(CODECOMPLETION_ROLE) } - fun getSelectedService(): ServiceType { - return serviceComboBox.selectedItem as ServiceType + fun getSelectedService(role: ModelRole): ServiceType { + return when(role) { + CHAT_ROLE -> serviceComboBox + CODECOMPLETION_ROLE -> codeCompletionServiceComboBox + }.selectedItem as ServiceType } - fun setSelectedService(serviceType: ServiceType) { - serviceComboBox.selectedItem = serviceType + fun setSelectedService(role: ModelRole, serviceType: ServiceType) { + when(role) { + CHAT_ROLE -> serviceComboBox + CODECOMPLETION_ROLE -> codeCompletionServiceComboBox + }.selectedItem = serviceType } fun getPanel(): JPanel = FormBuilder.createFormBuilder() @@ -39,6 +50,10 @@ class ServiceConfigurableComponent { CodeGPTBundle.get("settingsConfigurable.service.label"), serviceComboBox ) + .addLabeledComponent( + CodeGPTBundle.get("settingsConfigurable.service.codeCompletion.label"), + codeCompletionServiceComboBox + ) .addVerticalGap(8) .addComponent(JBLabel("All available providers that can be used with CodeGPT:")) .addVerticalGap(8) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettings.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettings.kt index e7321f4da..fe589ebf7 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettings.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettings.kt @@ -13,6 +13,7 @@ class OllamaSettings : class OllamaSettingsState : BaseState() { var host by string("http://localhost:11434") var model by string() + var codeCompletionModel by string() var codeCompletionsEnabled by property(false) var fimOverride by property(true) var fimTemplate by enum(InfillPromptTemplate.CODE_QWEN_2_5) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettingsForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettingsForm.kt index 17fb79102..3001b9d1e 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettingsForm.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettingsForm.kt @@ -19,13 +19,15 @@ import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.OllamaAp import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential import ee.carlrobert.codegpt.credentials.CredentialsStore.setCredential import ee.carlrobert.codegpt.settings.service.CodeCompletionConfigurationForm +import ee.carlrobert.codegpt.settings.service.ModelRole +import ee.carlrobert.codegpt.settings.service.ModelRole.* import ee.carlrobert.codegpt.ui.OverlayUtil import ee.carlrobert.codegpt.ui.UIUtil import ee.carlrobert.codegpt.ui.URLTextField import ee.carlrobert.llm.client.ollama.OllamaClient import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking -import java.awt.BorderLayout +import java.awt.Dimension import java.lang.String.format import java.net.ConnectException import java.util.concurrent.TimeoutException @@ -39,7 +41,7 @@ class OllamaSettingsForm { private val refreshModelsButton = JButton(CodeGPTBundle.get("settingsConfigurable.service.ollama.models.refresh")) private val hostField: JBTextField - private val modelComboBox: ComboBox + private val modelComboBoxes: Map> private val codeCompletionConfigurationForm: CodeCompletionConfigurationForm private val apiKeyField: JBPasswordField @@ -56,18 +58,24 @@ class OllamaSettingsForm { ) val emptyModelsComboBoxModel = DefaultComboBoxModel(arrayOf("Hit refresh to see models for this host")) - modelComboBox = ComboBox(emptyModelsComboBoxModel).apply { + modelComboBoxes = ModelRole.entries.associate { it to ComboBox(emptyModelsComboBoxModel).apply { isEnabled = false - } - hostField = URLTextField().apply { + preferredSize = Dimension(280, preferredSize.height) + } } + hostField = URLTextField(30).apply { text = settings.host whenTextChangedFromUi { - modelComboBox.model = emptyModelsComboBoxModel - modelComboBox.isEnabled = false + modelComboBoxes.values.forEach { comboBox -> + comboBox.model = emptyModelsComboBoxModel + comboBox.isEnabled = false + } } } refreshModelsButton.addActionListener { - refreshModels(getModel() ?: settings.model) + refreshModels(mapOf( + CHAT_ROLE to (getModel(CHAT_ROLE) ?: settings.model), + CODECOMPLETION_ROLE to (getModel(CODECOMPLETION_ROLE) ?: settings.codeCompletionModel), + )) } apiKeyField = JBPasswordField().apply { columns = 30 @@ -88,11 +96,13 @@ class OllamaSettingsForm { ) .addLabeledComponent( CodeGPTBundle.get("settingsConfigurable.shared.model.label"), - JPanel(BorderLayout(8, 0)).apply { - add(modelComboBox, BorderLayout.CENTER) - add(refreshModelsButton, BorderLayout.EAST) - } + modelComboBoxes[CHAT_ROLE]!! + ) + .addLabeledComponent( + CodeGPTBundle.get("settingsConfigurable.service.ollama.codeCompletionModel.label"), + modelComboBoxes[CODECOMPLETION_ROLE]!! ) + .addComponent(refreshModelsButton) .addComponent(TitledSeparator(CodeGPTBundle.get("settingsConfigurable.shared.authentication.title"))) .setFormLeftIndent(32) .addLabeledComponent( @@ -107,9 +117,9 @@ class OllamaSettingsForm { .addComponentFillVertically(JPanel(), 0) .panel - fun getModel(): String? { - return if (modelComboBox.isEnabled) { - modelComboBox.item + fun getModel(role: ModelRole): String? { + return if (modelComboBoxes[role]!!.isEnabled) { + modelComboBoxes[role]!!.item } else { null } @@ -120,7 +130,8 @@ class OllamaSettingsForm { fun resetForm() { service().state.run { hostField.text = host - modelComboBox.item = model ?: "" + modelComboBoxes[CHAT_ROLE]!!.item = model ?: "" + modelComboBoxes[CODECOMPLETION_ROLE]!!.item = codeCompletionModel ?: "" codeCompletionConfigurationForm.isCodeCompletionsEnabled = codeCompletionsEnabled codeCompletionConfigurationForm.fimTemplate = fimTemplate codeCompletionConfigurationForm.fimOverride != fimOverride @@ -131,7 +142,10 @@ class OllamaSettingsForm { fun applyChanges() { service().state.run { host = hostField.text - model = modelComboBox.item + if (modelComboBoxes[CHAT_ROLE]!!.isEnabled) + model = modelComboBoxes[CHAT_ROLE]!!.item + if (modelComboBoxes[CODECOMPLETION_ROLE]!!.isEnabled) + codeCompletionModel = modelComboBoxes[CODECOMPLETION_ROLE]!!.item codeCompletionsEnabled = codeCompletionConfigurationForm.isCodeCompletionsEnabled fimTemplate = codeCompletionConfigurationForm.fimTemplate!! fimOverride = codeCompletionConfigurationForm.fimOverride ?: false @@ -141,14 +155,15 @@ class OllamaSettingsForm { fun isModified() = service().state.run { hostField.text != host - || (modelComboBox.item != model && modelComboBox.isEnabled) + || (modelComboBoxes[CHAT_ROLE]!!.item != model && modelComboBoxes[CHAT_ROLE]!!.isEnabled) + || (modelComboBoxes[CODECOMPLETION_ROLE]!!.item != codeCompletionModel && modelComboBoxes[CODECOMPLETION_ROLE]!!.isEnabled) || codeCompletionConfigurationForm.isCodeCompletionsEnabled != codeCompletionsEnabled || codeCompletionConfigurationForm.fimTemplate != fimTemplate || codeCompletionConfigurationForm.fimOverride != fimOverride || getApiKey() != getCredential(OllamaApikey) } - private fun refreshModels(currentModel: String?) { + private fun refreshModels(currentModels: Map) { disableModelComboBoxWithPlaceholder(DefaultComboBoxModel(arrayOf("Loading"))) ReadAction.nonBlocking> { try { @@ -168,31 +183,33 @@ class OllamaSettingsForm { } } .finishOnUiThread(ModalityState.defaultModalityState()) { models -> - updateModelComboBoxState(models, currentModel) + updateModelComboBoxState(models, currentModels) } .submit(AppExecutorUtil.getAppExecutorService()) } - private fun updateModelComboBoxState(models: List, currentModel: String?) { + private fun updateModelComboBoxState(models: List, currentModels: Map) { if (models.isNotEmpty()) { - modelComboBox.model = DefaultComboBoxModel(models.toTypedArray()) - modelComboBox.isEnabled = true - currentModel?.let { - if (models.contains(currentModel)) { - modelComboBox.selectedItem = currentModel - } else { - OverlayUtil.showBalloon( - format( - CodeGPTBundle.get("validation.error.model.notExists"), - currentModel - ), - MessageType.ERROR, - modelComboBox - ) + modelComboBoxes.forEach { (role, comboBox) -> + comboBox.model = DefaultComboBoxModel(models.toTypedArray()) + comboBox.isEnabled = true + currentModels[role]?.let { + if (models.contains(currentModels[role])) { + comboBox.selectedItem = currentModels[role] + } else { + OverlayUtil.showBalloon( + format( + CodeGPTBundle.get("validation.error.model.notExists"), + currentModels[role] + ), + MessageType.ERROR, + comboBox + ) + } } } } else { - modelComboBox.model = DefaultComboBoxModel(arrayOf("No models")) + disableModelComboBoxWithPlaceholder(DefaultComboBoxModel(arrayOf("No models"))) } val availableModels = ApplicationManager.getApplication() .getService(OllamaSettings::class.java) @@ -229,9 +246,11 @@ class OllamaSettingsForm { } private fun disableModelComboBoxWithPlaceholder(placeholderModel: ComboBoxModel) { - modelComboBox.apply { - model = placeholderModel - isEnabled = false + modelComboBoxes.values.forEach { comboBox -> + comboBox.apply { + model = placeholderModel + isEnabled = false + } } } } diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index 7e08ac366..dc3cd84f9 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -39,6 +39,7 @@ settings.displayName=ProxyAI: Settings settings.openaiQuotaExceeded=OpenAI quota exceeded. settingsConfigurable.displayName.label=Display name: settingsConfigurable.service.label=Selected provider: +settingsConfigurable.service.codeCompletion.label=Code completion provider: settingsConfigurable.service.codegpt.apiKey.comment=You can find the API key in your User settings. settingsConfigurable.service.codegpt.chatCompletionModel.comment=Choose a model optimized for conversational interactions, including assistance with general queries and explanations. settingsConfigurable.service.codegpt.codeCompletionModel.comment=Choose a model tailored for code completion-related tasks. @@ -165,6 +166,7 @@ settingsConfigurable.prompts.exportDialog.exportError=Error exporting prompts se settingsConfigurable.prompts.exportDialog.title=Target File settingsConfigurable.prompts.importDialog.importError=Error importing prompts settings settingsConfigurable.service.ollama.models.refresh=Refresh Models +settingsConfigurable.service.ollama.codeCompletionModel.label=Model for code completion: advancedSettingsConfigurable.displayName=ProxyAI: Advanced Settings advancedSettingsConfigurable.proxy.title=HTTP/SOCKS Proxy advancedSettingsConfigurable.proxy.typeComboBoxField.label=Proxy: diff --git a/src/test/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.kt b/src/test/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.kt index e47041919..4159c3996 100644 --- a/src/test/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.kt +++ b/src/test/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.kt @@ -6,7 +6,9 @@ import com.intellij.openapi.editor.VisualPosition import com.intellij.openapi.util.TextRange import com.intellij.testFramework.PlatformTestUtil import ee.carlrobert.codegpt.CodeGPTKeys.REMAINING_EDITOR_COMPLETION +import ee.carlrobert.codegpt.completions.HuggingFaceModel import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings +import ee.carlrobert.codegpt.settings.service.ModelRole.* import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings import ee.carlrobert.codegpt.util.file.FileUtil import ee.carlrobert.llm.client.http.RequestEntity @@ -17,14 +19,15 @@ import testsupport.IntegrationTest class CodeCompletionServiceTest : IntegrationTest() { - fun `test code completion with ProxyAI provider`() { - useCodeGPTService() + fun `test code completion with OpenAI provider`() { + useOpenAIService("gpt-4", CODECOMPLETION_ROLE) service().state.nextEditsEnabled = false myFixture.configureByText( "CompletionTest.java", FileUtil.getResourceContent("/codecompletions/code-completion-file.txt") ) myFixture.editor.caretModel.moveToVisualPosition(VisualPosition(3, 0)) + project.service().clear() val prefix = """ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz @@ -37,12 +40,12 @@ class CodeCompletionServiceTest : IntegrationTest() { zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx """.trimIndent() - expectCodeGPT(StreamHttpExchange { request: RequestEntity -> - assertThat(request.uri.path).isEqualTo("/v1/code/completions") + expectOpenAI(StreamHttpExchange { request: RequestEntity -> + assertThat(request.uri.path).isEqualTo("/v1/completions") assertThat(request.method).isEqualTo("POST") assertThat(request.body) - .extracting("model", "prefix", "suffix", "fileExtension") - .containsExactly("TEST_CODE_MODEL", prefix, suffix, "java") + .extracting("model", "prompt", "suffix", "max_tokens") + .containsExactly("gpt-3.5-turbo-instruct", prefix, suffix, 128) listOf( jsonMapResponse("choices", jsonArray(jsonMap("text", "ublic "))), jsonMapResponse("choices", jsonArray(jsonMap("text", "void"))), @@ -57,14 +60,14 @@ class CodeCompletionServiceTest : IntegrationTest() { } } - fun `test code completion with OpenAI provider`() { - useOpenAIService() - service().state.nextEditsEnabled = false + fun `test code completion with Ollama provider and separate model settings`() { + useOllamaService(CODECOMPLETION_ROLE) myFixture.configureByText( "CompletionTest.java", FileUtil.getResourceContent("/codecompletions/code-completion-file.txt") ) myFixture.editor.caretModel.moveToVisualPosition(VisualPosition(3, 0)) + project.service().clear() val prefix = """ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz @@ -77,34 +80,38 @@ class CodeCompletionServiceTest : IntegrationTest() { zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx """.trimIndent() - expectOpenAI(StreamHttpExchange { request: RequestEntity -> - assertThat(request.uri.path).isEqualTo("/v1/completions") + expectOllama(StreamHttpExchange { request: RequestEntity -> + assertThat(request.uri.path).isEqualTo("/api/generate") assertThat(request.method).isEqualTo("POST") assertThat(request.body) - .extracting("model", "prompt", "suffix", "max_tokens") - .containsExactly("gpt-3.5-turbo-instruct", prefix, suffix, 128) + .extracting("model", "prompt", "suffix") + .containsExactly(HuggingFaceModel.CODE_QWEN_2_5_3B_Q4_K_M.code, prefix, suffix) listOf( - jsonMapResponse("choices", jsonArray(jsonMap("text", "ublic "))), - jsonMapResponse("choices", jsonArray(jsonMap("text", "void"))), - jsonMapResponse("choices", jsonArray(jsonMap("text", " main"))) + jsonMapResponse( + e("model", HuggingFaceModel.CODE_QWEN_2_5_3B_Q4_K_M.code), + e("created_at", "2023-08-04T08:52:19.385406455-07:00"), + e("response", "rivate void main"), + e("done", true), + ), ) }) myFixture.type('p') assertInlineSuggestion("Failed to display initial inline suggestion.") { - "ublic void main" == it + "rivate void main" == it } } fun `test apply next partial completion word`() { - useLlamaService(true) + useLlamaService(true, CODECOMPLETION_ROLE) service().state.nextEditsEnabled = false myFixture.configureByText( "CompletionTest.java", FileUtil.getResourceContent("/codecompletions/code-completion-file.txt") ) myFixture.editor.caretModel.moveToVisualPosition(VisualPosition(3, 0)) + project.service().clear() val expectedCompletion = "public void main" val prefix = """ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx @@ -153,13 +160,14 @@ class CodeCompletionServiceTest : IntegrationTest() { } fun `_test apply inline suggestions without initial following text`() { - useCodeGPTService() + useCodeGPTService(CODECOMPLETION_ROLE) service().state.nextEditsEnabled = false myFixture.configureByText( "CompletionTest.java", "class Node {\n " ) myFixture.editor.caretModel.moveToVisualPosition(VisualPosition(1, 2)) + project.service().clear() expectCodeGPT(StreamHttpExchange { request: RequestEntity -> assertThat(request.uri.path).isEqualTo("/v1/code/completions") assertThat(request.method).isEqualTo("POST") @@ -270,13 +278,14 @@ class CodeCompletionServiceTest : IntegrationTest() { } fun `_test apply inline suggestions with initial following text`() { - useCodeGPTService() + useCodeGPTService(CODECOMPLETION_ROLE) service().state.nextEditsEnabled = false myFixture.configureByText( "CompletionTest.java", "if () {\n \n} else {\n}" ) myFixture.editor.caretModel.moveToVisualPosition(VisualPosition(0, 4)) + project.service().clear() expectCodeGPT(StreamHttpExchange { request: RequestEntity -> assertThat(request.uri.path).isEqualTo("/v1/code/completions") assertThat(request.method).isEqualTo("POST") @@ -341,7 +350,7 @@ class CodeCompletionServiceTest : IntegrationTest() { } fun `_test adjust completion line whitespaces`() { - useCodeGPTService() + useCodeGPTService(CODECOMPLETION_ROLE) service().state.nextEditsEnabled = false myFixture.configureByText( "CompletionTest.java", @@ -350,6 +359,7 @@ class CodeCompletionServiceTest : IntegrationTest() { "}" ) myFixture.editor.caretModel.moveToVisualPosition(VisualPosition(1, 3)) + project.service().clear() expectCodeGPT(StreamHttpExchange { request: RequestEntity -> assertThat(request.uri.path).isEqualTo("/v1/code/completions") assertThat(request.method).isEqualTo("POST") diff --git a/src/test/kotlin/testsupport/mixin/ShortcutsTestMixin.kt b/src/test/kotlin/testsupport/mixin/ShortcutsTestMixin.kt index 5df5f639b..fabe800a1 100644 --- a/src/test/kotlin/testsupport/mixin/ShortcutsTestMixin.kt +++ b/src/test/kotlin/testsupport/mixin/ShortcutsTestMixin.kt @@ -6,6 +6,8 @@ import ee.carlrobert.codegpt.completions.HuggingFaceModel import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.* import ee.carlrobert.codegpt.credentials.CredentialsStore.setCredential import ee.carlrobert.codegpt.settings.GeneralSettings +import ee.carlrobert.codegpt.settings.service.ModelRole +import ee.carlrobert.codegpt.settings.service.ModelRole.* import ee.carlrobert.codegpt.settings.service.ServiceType import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings import ee.carlrobert.codegpt.settings.service.google.GoogleSettings @@ -17,8 +19,8 @@ import java.util.function.BooleanSupplier interface ShortcutsTestMixin { - fun useCodeGPTService() { - service().state.selectedService = ServiceType.CODEGPT + fun useCodeGPTService(role: ModelRole = CHAT_ROLE) { + service().state.setSelectedService(role,ServiceType.CODEGPT) setCredential(CodeGptApiKey, "TEST_API_KEY") service().state.run { chatCompletionSettings.model = "TEST_MODEL" @@ -27,8 +29,8 @@ interface ShortcutsTestMixin { } } - fun useOpenAIService(chatModel: String? = "gpt-4o") { - service().state.selectedService = ServiceType.OPENAI + fun useOpenAIService(chatModel: String? = "gpt-4o", role: ModelRole = CHAT_ROLE) { + service().state.setSelectedService(role, ServiceType.OPENAI) setCredential(OpenaiApiKey, "TEST_API_KEY") service().state.run { model = chatModel @@ -36,24 +38,27 @@ interface ShortcutsTestMixin { } } - fun useLlamaService(codeCompletionsEnabled: Boolean = false) { - GeneralSettings.getCurrentState().selectedService = ServiceType.LLAMA_CPP + fun useLlamaService(codeCompletionsEnabled: Boolean = false, role: ModelRole = CHAT_ROLE) { + GeneralSettings.getCurrentState().setSelectedService(role,ServiceType.LLAMA_CPP) LlamaSettings.getCurrentState().serverPort = null LlamaSettings.getCurrentState().isCodeCompletionsEnabled = codeCompletionsEnabled LlamaSettings.getCurrentState().huggingFaceModel = HuggingFaceModel.CODE_LLAMA_7B_Q4 } - fun useOllamaService() { - GeneralSettings.getCurrentState().selectedService = ServiceType.OLLAMA + fun useOllamaService(role: ModelRole = CHAT_ROLE) { + GeneralSettings.getCurrentState().setSelectedService(role, ServiceType.OLLAMA) setCredential(OllamaApikey, "TEST_API_KEY") service().state.apply { model = HuggingFaceModel.LLAMA_3_8B_Q6_K.code + codeCompletionModel = HuggingFaceModel.CODE_QWEN_2_5_3B_Q4_K_M.code + codeCompletionsEnabled = true + fimOverride = false host = null } } - fun useGoogleService() { - GeneralSettings.getCurrentState().selectedService = ServiceType.GOOGLE + fun useGoogleService(role: ModelRole = CHAT_ROLE) { + GeneralSettings.getCurrentState().setSelectedService(role, ServiceType.GOOGLE) setCredential(GoogleApiKey, "TEST_API_KEY") service().state.model = GoogleModel.GEMINI_PRO.code }