From 0852c2717057a49fd2acf0866624d43f01b6397f Mon Sep 17 00:00:00 2001 From: Carl-Robert Date: Wed, 8 May 2024 23:59:51 +0300 Subject: [PATCH] feat: add CodeGPT "native" API provider (#537) * feat: support codegpt client * feat: add basic request handler test * refactor: minor cleanup --- gradle/libs.versions.toml | 2 +- .../java/ee/carlrobert/codegpt/Icons.java | 2 + .../completions/CompletionClientProvider.java | 11 +- .../CompletionRequestProvider.java | 2 +- .../completions/CompletionRequestService.java | 49 +++--- .../codegpt/conversations/Conversation.java | 4 +- .../conversations/ConversationService.java | 17 ++- .../codegpt/settings/GeneralSettings.java | 6 + .../settings/GeneralSettingsComponent.java | 18 ++- .../settings/GeneralSettingsConfigurable.java | 9 ++ .../settings/GeneralSettingsState.java | 2 +- .../codegpt/settings/service/ServiceType.java | 1 + .../service/anthropic/AnthropicSettings.java | 2 +- .../anthropic/AnthropicSettingsForm.java | 4 +- .../settings/service/azure/AzureSettings.java | 4 +- .../service/azure/AzureSettingsForm.java | 10 +- .../settings/service/llama/LlamaSettings.java | 2 +- .../form/LlamaServerPreferencesForm.java | 4 +- .../service/openai/OpenAISettings.java | 2 +- .../service/openai/OpenAISettingsForm.java | 8 +- .../settings/service/you/YouSettingsForm.java | 2 +- .../ui/ChatToolWindowScrollablePanel.java | 23 +++ .../chat/ui/textarea/ModelComboBoxAction.java | 128 +++++++++++----- .../CodeCompletionFeatureToggleActions.kt | 23 +-- .../CodeCompletionRequestFactory.kt | 13 ++ .../codecompletions/CodeCompletionService.kt | 59 ++++++++ .../CodeGPTInlineCompletionProvider.kt | 8 +- .../codegpt/credentials/CredentialsStore.kt | 5 +- .../service/codegpt/CodeGPTServiceForm.kt | 141 ++++++++++++++++++ .../service/codegpt/CodeGPTServiceSettings.kt | 26 ++++ src/main/resources/icons/codegpt-model.svg | 2 + .../resources/icons/codegpt-model_dark.svg | 2 + .../resources/messages/codegpt.properties | 4 +- .../CompletionRequestProviderTest.kt | 8 +- .../DefaultCompletionRequestHandlerTest.kt | 31 ++++ .../testsupport/mixin/ShortcutsTestMixin.kt | 9 +- 36 files changed, 510 insertions(+), 133 deletions(-) create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionService.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceForm.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceSettings.kt create mode 100644 src/main/resources/icons/codegpt-model.svg create mode 100644 src/main/resources/icons/codegpt-model_dark.svg diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1dc9d9a5..58704e27 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ jsoup = "1.17.2" jtokkit = "1.0.0" junit = "5.10.2" kotlin = "1.9.23" -llm-client = "0.8.0" +llm-client = "0.8.1" okio = "3.9.0" tree-sitter = "0.22.5" diff --git a/src/main/java/ee/carlrobert/codegpt/Icons.java b/src/main/java/ee/carlrobert/codegpt/Icons.java index 89467c2c..34311e2a 100644 --- a/src/main/java/ee/carlrobert/codegpt/Icons.java +++ b/src/main/java/ee/carlrobert/codegpt/Icons.java @@ -8,6 +8,8 @@ public final class Icons { public static final Icon Default = IconLoader.getIcon("/icons/codegpt.svg", Icons.class); public static final Icon DefaultSmall = IconLoader.getIcon("/icons/codegpt-small.svg", Icons.class); + public static final Icon CodeGPTModel = + IconLoader.getIcon("/icons/codegpt-model.svg", Icons.class); public static final Icon Anthropic = IconLoader.getIcon("/icons/anthropic.svg", Icons.class); public static final Icon Azure = IconLoader.getIcon("/icons/azure.svg", Icons.class); public static final Icon Google = IconLoader.getIcon("/icons/google.svg", Icons.class); diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java index 40435aa0..6f4ce8a9 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java @@ -1,9 +1,10 @@ package ee.carlrobert.codegpt.completions; +import static ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential; + import com.intellij.openapi.application.ApplicationManager; import ee.carlrobert.codegpt.CodeGPTPlugin; import ee.carlrobert.codegpt.completions.you.YouUserManager; -import ee.carlrobert.codegpt.credentials.CredentialsStore; import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey; import ee.carlrobert.codegpt.settings.advanced.AdvancedSettings; import ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettings; @@ -14,6 +15,7 @@ import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; import ee.carlrobert.llm.client.anthropic.ClaudeClient; import ee.carlrobert.llm.client.azure.AzureClient; import ee.carlrobert.llm.client.azure.AzureCompletionRequestParams; +import ee.carlrobert.llm.client.codegpt.CodeGPTClient; import ee.carlrobert.llm.client.google.GoogleClient; import ee.carlrobert.llm.client.llama.LlamaClient; import ee.carlrobert.llm.client.ollama.OllamaClient; @@ -25,12 +27,13 @@ import java.net.Proxy; import java.util.concurrent.TimeUnit; import okhttp3.Credentials; import okhttp3.OkHttpClient; -import org.jetbrains.annotations.Nullable; public class CompletionClientProvider { - private static @Nullable String getCredential(CredentialKey key) { - return CredentialsStore.INSTANCE.getCredential(key); + public static CodeGPTClient getCodeGPTClient() { + return new CodeGPTClient( + getCredential(CredentialKey.CODEGPT_API_KEY), + getDefaultClientBuilder()); } public static OpenAIClient getOpenAIClient() { diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java index 9742b245..376df97e 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java @@ -251,7 +251,7 @@ public class CompletionRequestProvider { List messages, boolean streamRequest) { return buildCustomOpenAIChatCompletionRequest(settings, messages, streamRequest, - CredentialsStore.INSTANCE.getCredential(CUSTOM_SERVICE_API_KEY)); + CredentialsStore.getCredential(CUSTOM_SERVICE_API_KEY)); } private static Request buildCustomOpenAIChatCompletionRequest( diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java index 47e117a9..d3592f69 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java @@ -8,8 +8,6 @@ import static ee.carlrobert.codegpt.settings.service.ServiceType.YOU; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.Service; import com.intellij.openapi.diagnostic.Logger; -import ee.carlrobert.codegpt.codecompletions.CodeCompletionRequestFactory; -import ee.carlrobert.codegpt.codecompletions.InfillRequestDetails; import ee.carlrobert.codegpt.completions.llama.LlamaModel; import ee.carlrobert.codegpt.completions.llama.PromptTemplate; import ee.carlrobert.codegpt.credentials.CredentialsStore; @@ -19,6 +17,7 @@ import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings; import ee.carlrobert.codegpt.settings.service.ServiceType; import ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettings; import ee.carlrobert.codegpt.settings.service.azure.AzureSettings; +import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings; import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings; import ee.carlrobert.codegpt.settings.service.google.GoogleSettings; import ee.carlrobert.codegpt.settings.service.google.GoogleSettingsState; @@ -83,8 +82,18 @@ public final class CompletionRequestService { public EventSource getChatCompletionAsync( CallParameters callParameters, CompletionEventListener eventListener) { + var application = ApplicationManager.getApplication(); var requestProvider = new CompletionRequestProvider(callParameters.getConversation()); return switch (GeneralSettings.getCurrentState().getSelectedService()) { + case CODEGPT -> CompletionClientProvider.getCodeGPTClient().getChatCompletionAsync( + requestProvider.buildOpenAIChatCompletionRequest( + application.getService(CodeGPTServiceSettings.class) + .getState() + .getChatCompletionSettings() + .getModel(), + callParameters), + eventListener + ); case OPENAI -> CompletionClientProvider.getOpenAIClient().getChatCompletionAsync( requestProvider.buildOpenAIChatCompletionRequest( OpenAISettings.getCurrentState().getModel(), @@ -92,8 +101,7 @@ public final class CompletionRequestService { eventListener); case CUSTOM_OPENAI -> getCustomOpenAIChatCompletionAsync( requestProvider.buildCustomOpenAIChatCompletionRequest( - ApplicationManager.getApplication() - .getService(CustomServiceSettings.class) + application.getService(CustomServiceSettings.class) .getState() .getChatCompletionSettings(), callParameters), @@ -116,8 +124,7 @@ public final class CompletionRequestService { requestProvider.buildOllamaChatCompletionRequest(callParameters), eventListener); case GOOGLE -> { - var settings = ApplicationManager.getApplication() - .getService(GoogleSettings.class).getState(); + var settings = application.getService(GoogleSettings.class).getState(); yield CompletionClientProvider.getGoogleClient().getChatCompletionAsync( requestProvider.buildGoogleChatCompletionRequest( settings.getModel(), @@ -128,30 +135,6 @@ public final class CompletionRequestService { }; } - public EventSource getCodeCompletionAsync( - InfillRequestDetails requestDetails, - CompletionEventListener eventListener) { - var httpClient = CompletionClientProvider.getDefaultClientBuilder().build(); - return switch (GeneralSettings.getCurrentState().getSelectedService()) { - case OPENAI -> CompletionClientProvider.getOpenAIClient() - .getCompletionAsync( - CodeCompletionRequestFactory.buildOpenAIRequest(requestDetails), - eventListener); - case CUSTOM_OPENAI -> EventSources.createFactory(httpClient).newEventSource( - CodeCompletionRequestFactory.buildCustomRequest(requestDetails), - new OpenAITextCompletionEventSourceListener(eventListener)); - case LLAMA_CPP -> CompletionClientProvider.getLlamaClient() - .getChatCompletionAsync( - CodeCompletionRequestFactory.buildLlamaRequest(requestDetails), - eventListener); - case OLLAMA -> CompletionClientProvider.getOllamaClient().getCompletionAsync( - CodeCompletionRequestFactory.INSTANCE.buildOllamaRequest(requestDetails), - eventListener); - default -> - throw new IllegalArgumentException("Code completion not supported for selected service"); - }; - } - public void generateCommitMessageAsync( String systemPrompt, String gitDiff, @@ -164,6 +147,10 @@ public final class CompletionRequestService { .build(); var selectedService = GeneralSettings.getCurrentState().getSelectedService(); switch (selectedService) { + case CODEGPT: + CompletionClientProvider.getCodeGPTClient() + .getChatCompletionAsync(openaiRequest, eventListener); + break; case OPENAI: CompletionClientProvider.getOpenAIClient() .getChatCompletionAsync(openaiRequest, eventListener); @@ -283,7 +270,7 @@ public final class CompletionRequestService { AzureSettings.getCurrentState().isUseAzureApiKeyAuthentication() ? CredentialKey.AZURE_OPENAI_API_KEY : CredentialKey.AZURE_ACTIVE_DIRECTORY_TOKEN); - case CUSTOM_OPENAI, ANTHROPIC, LLAMA_CPP, OLLAMA -> true; + case CODEGPT, CUSTOM_OPENAI, ANTHROPIC, LLAMA_CPP, OLLAMA -> true; case YOU -> false; case GOOGLE -> CredentialsStore.INSTANCE.isCredentialSet(CredentialKey.GOOGLE_API_KEY); }; diff --git a/src/main/java/ee/carlrobert/codegpt/conversations/Conversation.java b/src/main/java/ee/carlrobert/codegpt/conversations/Conversation.java index 7d3b0265..83a32085 100644 --- a/src/main/java/ee/carlrobert/codegpt/conversations/Conversation.java +++ b/src/main/java/ee/carlrobert/codegpt/conversations/Conversation.java @@ -29,7 +29,7 @@ public class Conversation { } public void setMessages(List messages) { - this.messages = messages; + this.messages = new ArrayList<>(messages); } public String getClientCode() { @@ -77,7 +77,7 @@ public class Conversation { } public void removeMessage(UUID messageId) { - setMessages(messages.stream() + messages = new ArrayList<>(messages.stream() .filter(message -> !message.getId().equals(messageId)) .toList()); } diff --git a/src/main/java/ee/carlrobert/codegpt/conversations/ConversationService.java b/src/main/java/ee/carlrobert/codegpt/conversations/ConversationService.java index d11a29e3..3d5352b7 100644 --- a/src/main/java/ee/carlrobert/codegpt/conversations/ConversationService.java +++ b/src/main/java/ee/carlrobert/codegpt/conversations/ConversationService.java @@ -8,6 +8,7 @@ import ee.carlrobert.codegpt.settings.GeneralSettings; import ee.carlrobert.codegpt.settings.service.ServiceType; import ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettings; import ee.carlrobert.codegpt.settings.service.azure.AzureSettings; +import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings; import ee.carlrobert.codegpt.settings.service.google.GoogleSettings; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings; @@ -188,7 +189,12 @@ public final class ConversationService { } private static String getModelForSelectedService(ServiceType serviceType) { + var application = ApplicationManager.getApplication(); return switch (serviceType) { + case CODEGPT -> application.getService(CodeGPTServiceSettings.class) + .getState() + .getChatCompletionSettings() + .getModel(); case OPENAI -> OpenAISettings.getCurrentState().getModel(); case CUSTOM_OPENAI -> "CustomService"; case ANTHROPIC -> AnthropicSettings.getCurrentState().getModel(); @@ -200,15 +206,12 @@ public final class ConversationService { ? llamaSettings.getCustomLlamaModelPath() : llamaSettings.getHuggingFaceModel().getCode(); } - case OLLAMA -> ApplicationManager.getApplication() - .getService(OllamaSettings.class) + case OLLAMA -> application.getService(OllamaSettings.class) + .getState() + .getModel(); + case GOOGLE -> application.getService(GoogleSettings.class) .getState() .getModel(); - case GOOGLE -> - ApplicationManager.getApplication() - .getService(GoogleSettings.class) - .getState() - .getModel(); }; } } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettings.java b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettings.java index b72ee03d..39a55745 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettings.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettings.java @@ -10,6 +10,7 @@ import ee.carlrobert.codegpt.conversations.Conversation; import ee.carlrobert.codegpt.settings.service.ServiceType; import ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettings; import ee.carlrobert.codegpt.settings.service.azure.AzureSettings; +import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings; import ee.carlrobert.codegpt.settings.service.google.GoogleSettings; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings; @@ -78,6 +79,11 @@ public class GeneralSettings implements PersistentStateComponent serviceComboBox; + private final CodeGPTServiceForm codeGPTSettingsForm; private final OpenAISettingsForm openAISettingsForm; private final CustomServiceForm customConfigurationSettingsForm; private final AnthropicSettingsForm anthropicSettingsForm; @@ -54,6 +56,7 @@ public class GeneralSettingsComponent { public GeneralSettingsComponent(Disposable parentDisposable, GeneralSettings settings) { displayNameField = new JBTextField(settings.getState().getDisplayName(), 20); + codeGPTSettingsForm = new CodeGPTServiceForm(); openAISettingsForm = new OpenAISettingsForm(OpenAISettings.getCurrentState()); customConfigurationSettingsForm = new CustomServiceForm(); anthropicSettingsForm = new AnthropicSettingsForm(AnthropicSettings.getCurrentState()); @@ -65,6 +68,7 @@ public class GeneralSettingsComponent { var cardLayout = new DynamicCardLayout(); var cards = new JPanel(cardLayout); + cards.add(codeGPTSettingsForm.getForm(), CODEGPT.getCode()); cards.add(openAISettingsForm.getForm(), OPENAI.getCode()); cards.add(customConfigurationSettingsForm.getForm(), CUSTOM_OPENAI.getCode()); cards.add(anthropicSettingsForm.getForm(), ANTHROPIC.getCode()); @@ -73,10 +77,9 @@ public class GeneralSettingsComponent { cards.add(llamaSettingsForm, LLAMA_CPP.getCode()); cards.add(ollamaSettingsForm.getForm(), OLLAMA.getCode()); cards.add(googleSettingsForm.getForm(), GOOGLE.getCode()); - var serviceComboBoxModel = new DefaultComboBoxModel(); - serviceComboBoxModel.addAll(Arrays.stream(ServiceType.values()).toList()); - serviceComboBox = new ComboBox<>(serviceComboBoxModel); - serviceComboBox.setSelectedItem(OPENAI); + cardLayout.show(cards, settings.getState().getSelectedService().getCode()); + serviceComboBox = new ComboBox<>(new DefaultComboBoxModel<>(ServiceType.values())); + serviceComboBox.setSelectedItem(settings.getState().getSelectedService()); serviceComboBox.setPreferredSize(displayNameField.getPreferredSize()); serviceComboBox.addItemListener(e -> { ServiceType selectedService = (ServiceType) e.getItem(); @@ -97,6 +100,10 @@ public class GeneralSettingsComponent { .getPanel(); } + public CodeGPTServiceForm getCodeGPTSettingsForm() { + return codeGPTSettingsForm; + } + public OpenAISettingsForm getOpenAISettingsForm() { return openAISettingsForm; } @@ -154,6 +161,7 @@ public class GeneralSettingsComponent { } public void resetForms() { + codeGPTSettingsForm.resetForm(); openAISettingsForm.resetForm(); customConfigurationSettingsForm.resetForm(); anthropicSettingsForm.resetForm(); diff --git a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java index cef436f8..759788da 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java @@ -3,6 +3,7 @@ package ee.carlrobert.codegpt.settings; import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.ANTHROPIC_API_KEY; import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.AZURE_ACTIVE_DIRECTORY_TOKEN; import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.AZURE_OPENAI_API_KEY; +import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CODEGPT_API_KEY; import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CUSTOM_SERVICE_API_KEY; import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.GOOGLE_API_KEY; import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.LLAMA_API_KEY; @@ -18,6 +19,7 @@ import ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettings; import ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettingsForm; import ee.carlrobert.codegpt.settings.service.azure.AzureSettings; import ee.carlrobert.codegpt.settings.service.azure.AzureSettingsForm; +import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceForm; import ee.carlrobert.codegpt.settings.service.custom.CustomServiceForm; import ee.carlrobert.codegpt.settings.service.google.GoogleSettingsForm; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; @@ -65,6 +67,7 @@ public class GeneralSettingsConfigurable implements Configurable { return !component.getDisplayName().equals(settings.getDisplayName()) || component.getSelectedService() != settings.getSelectedService() + || component.getCodeGPTSettingsForm().isModified() || OpenAISettings.getInstance().isModified(component.getOpenAISettingsForm()) || component.getCustomConfigurationSettingsForm().isModified() || AnthropicSettings.getInstance().isModified(component.getAnthropicSettingsForm()) @@ -81,6 +84,7 @@ public class GeneralSettingsConfigurable implements Configurable { settings.setDisplayName(component.getDisplayName()); settings.setSelectedService(component.getSelectedService()); + applyCodeGPTServiceSettings(component.getCodeGPTSettingsForm()); var openAISettingsForm = component.getOpenAISettingsForm(); applyOpenAISettings(openAISettingsForm); applyCustomOpenAISettings(component.getCustomConfigurationSettingsForm()); @@ -104,6 +108,11 @@ public class GeneralSettingsConfigurable implements Configurable { } } + private void applyCodeGPTServiceSettings(CodeGPTServiceForm form) { + CredentialsStore.INSTANCE.setCredential(CODEGPT_API_KEY, form.getApiKey()); + form.applyChanges(); + } + private void applyOpenAISettings(OpenAISettingsForm form) { CredentialsStore.INSTANCE.setCredential(OPENAI_API_KEY, form.getApiKey()); OpenAISettings.getInstance().loadState(form.getCurrentState()); diff --git a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsState.java b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsState.java index 2c266c13..cd13df1f 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsState.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsState.java @@ -5,7 +5,7 @@ import ee.carlrobert.codegpt.settings.service.ServiceType; public class GeneralSettingsState { private String displayName = ""; - private ServiceType selectedService = ServiceType.OPENAI; + private ServiceType selectedService = ServiceType.CODEGPT; public String getDisplayName() { if (displayName == null || displayName.isEmpty()) { diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceType.java b/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceType.java index dc749997..514afbac 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceType.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceType.java @@ -3,6 +3,7 @@ package ee.carlrobert.codegpt.settings.service; import ee.carlrobert.codegpt.CodeGPTBundle; public enum ServiceType { + CODEGPT("CODEGPT", "service.codegpt.title", "codegpt.chat.completion"), OPENAI("OPENAI", "service.openai.title", "chat.completion"), CUSTOM_OPENAI("CUSTOM_OPENAI", "service.custom.openai.title", "custom.openai.chat.completion"), ANTHROPIC("ANTHROPIC", "service.anthropic.title", "anthropic.chat.completion"), diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/anthropic/AnthropicSettings.java b/src/main/java/ee/carlrobert/codegpt/settings/service/anthropic/AnthropicSettings.java index 4ea11a0c..c488a8b8 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/anthropic/AnthropicSettings.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/anthropic/AnthropicSettings.java @@ -37,6 +37,6 @@ public class AnthropicSettings implements PersistentStateComponent( new EnumComboBoxModel<>(OpenAIChatCompletionModel.class)); @@ -58,10 +58,10 @@ public class OpenAISettingsForm { .createPanel(); return FormBuilder.createFormBuilder() - .addComponent(new TitledSeparator(CodeGPTBundle.get("shared.codeCompletions"))) - .addComponent(withEmptyLeftBorder(codeCompletionConfigurationForm.getForm())) .addComponent(new TitledSeparator(CodeGPTBundle.get("shared.configuration"))) .addComponent(withEmptyLeftBorder(configurationGrid)) + .addComponent(new TitledSeparator(CodeGPTBundle.get("shared.codeCompletions"))) + .addComponent(withEmptyLeftBorder(codeCompletionConfigurationForm.getForm())) .addComponentFillVertically(new JPanel(), 0) .getPanel(); } @@ -88,7 +88,7 @@ public class OpenAISettingsForm { public void resetForm() { var state = OpenAISettings.getCurrentState(); - apiKeyField.setText(CredentialsStore.INSTANCE.getCredential(OPENAI_API_KEY)); + apiKeyField.setText(CredentialsStore.getCredential(OPENAI_API_KEY)); completionModelComboBox.setSelectedItem( OpenAIChatCompletionModel.findByCode(state.getModel())); organizationField.setText(state.getOrganization()); diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/you/YouSettingsForm.java b/src/main/java/ee/carlrobert/codegpt/settings/service/you/YouSettingsForm.java index f3def2ed..a834b331 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/you/YouSettingsForm.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/you/YouSettingsForm.java @@ -53,7 +53,7 @@ public class YouSettingsForm extends JPanel { passwordField = new JBPasswordField(); passwordField.setColumns(25); if (!settings.getEmail().isEmpty()) { - passwordField.setText(CredentialsStore.INSTANCE.getCredential(YOU_ACCOUNT_PASSWORD)); + passwordField.setText(CredentialsStore.getCredential(YOU_ACCOUNT_PASSWORD)); } signInButton = new JButton(CodeGPTBundle.get("settingsConfigurable.service.you.signIn.label")); signUpTextPane = createSignUpTextPane(); diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatToolWindowScrollablePanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatToolWindowScrollablePanel.java index 02deaabd..c4b0bfca 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatToolWindowScrollablePanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatToolWindowScrollablePanel.java @@ -2,6 +2,13 @@ package ee.carlrobert.codegpt.toolwindow.chat.ui; import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel; import com.intellij.openapi.roots.ui.componentsList.layout.VerticalStackLayout; +import com.intellij.ui.JBColor; +import com.intellij.util.ui.JBUI; +import ee.carlrobert.codegpt.credentials.CredentialsStore; +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey; +import ee.carlrobert.codegpt.settings.GeneralSettings; +import ee.carlrobert.codegpt.settings.service.ServiceType; +import ee.carlrobert.codegpt.ui.UIUtil; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -21,6 +28,22 @@ public class ChatToolWindowScrollablePanel extends ScrollablePanel { public void displayLandingView(JComponent landingView) { clearAll(); add(landingView); + if (GeneralSettings.getCurrentState().getSelectedService() == ServiceType.CODEGPT + && !CredentialsStore.INSTANCE.isCredentialSet(CredentialKey.CODEGPT_API_KEY)) { + + var panel = new ResponsePanel() + .addContent(UIUtil.createTextPane(""" + +

+ It looks like you haven't configured your API key yet. Visit the CodeGPT settings to do so. +

+

+ Don't have an account? Sign up for free access to all open-source models. +

+ """, false, UIUtil::handleHyperlinkClicked)); + panel.setBorder(JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0)); + add(panel); + } } public ResponsePanel getMessageResponsePanel(UUID messageId) { diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java index 5a22d28d..52930eaf 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java @@ -1,9 +1,11 @@ package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea; +import static ee.carlrobert.codegpt.settings.service.ServiceType.CODEGPT; import static ee.carlrobert.codegpt.settings.service.ServiceType.CUSTOM_OPENAI; import static ee.carlrobert.codegpt.settings.service.ServiceType.OLLAMA; import static ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI; import static ee.carlrobert.codegpt.settings.service.ServiceType.YOU; +import static ee.carlrobert.llm.client.codegpt.CodeGPTAvailableModels.AVAILABLE_CHAT_MODELS; import static java.lang.String.format; import com.intellij.openapi.actionSystem.ActionUpdateThread; @@ -19,17 +21,17 @@ import ee.carlrobert.codegpt.Icons; import ee.carlrobert.codegpt.completions.llama.LlamaModel; import ee.carlrobert.codegpt.completions.you.YouUserManager; import ee.carlrobert.codegpt.completions.you.auth.SignedOutNotifier; +import ee.carlrobert.codegpt.credentials.CredentialsStore; +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey; import ee.carlrobert.codegpt.settings.GeneralSettings; -import ee.carlrobert.codegpt.settings.GeneralSettingsState; import ee.carlrobert.codegpt.settings.service.ServiceType; +import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings; import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings; -import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettingsState; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; -import ee.carlrobert.codegpt.settings.service.openai.OpenAISettingsState; import ee.carlrobert.codegpt.settings.service.you.YouSettings; -import ee.carlrobert.codegpt.settings.service.you.YouSettingsState; +import ee.carlrobert.llm.client.codegpt.CodeGPTModel; import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel; import ee.carlrobert.llm.client.you.completion.YouCompletionCustomModel; import ee.carlrobert.llm.client.you.completion.YouCompletionMode; @@ -41,19 +43,9 @@ import org.jetbrains.annotations.NotNull; public class ModelComboBoxAction extends ComboBoxAction { private final Runnable onModelChange; - private final GeneralSettingsState settings; - private final OpenAISettingsState openAISettings; - private final YouSettingsState youSettings; - private final OllamaSettingsState ollamaSettings; public ModelComboBoxAction(Runnable onModelChange, ServiceType selectedService) { this.onModelChange = onModelChange; - settings = GeneralSettings.getCurrentState(); - openAISettings = OpenAISettings.getCurrentState(); - youSettings = YouSettings.getCurrentState(); - ollamaSettings = ApplicationManager.getApplication() - .getService(OllamaSettings.class) - .getState(); updateTemplatePresentation(selectedService); subscribeToYouSignedOutTopic(ApplicationManager.getApplication().getMessageBus().connect()); @@ -73,18 +65,28 @@ public class ModelComboBoxAction extends ComboBoxAction { return button; } + private AnAction[] getCodeGPTModelActions(Presentation presentation) { + var apiKey = CredentialsStore.getCredential(CredentialKey.CODEGPT_API_KEY); + return AVAILABLE_CHAT_MODELS.stream() + .map(model -> { + var enabled = "codellama/CodeLlama-13b-Instruct-hf".equals(model.getCode()) + || (apiKey != null && !apiKey.isEmpty()); + return createCodeGPTModelAction(model, enabled, presentation); + }) + .toArray(AnAction[]::new); + } + @Override protected @NotNull DefaultActionGroup createPopupActionGroup(JComponent button) { var presentation = ((ComboBoxButton) button).getPresentation(); var actionGroup = new DefaultActionGroup(); + actionGroup.addSeparator("CodeGPT"); + actionGroup.addAll(getCodeGPTModelActions(presentation)); actionGroup.addSeparator("OpenAI"); List.of( OpenAIChatCompletionModel.GPT_4_VISION_PREVIEW, OpenAIChatCompletionModel.GPT_4_0125_128k, - OpenAIChatCompletionModel.GPT_3_5_0125_16k, - OpenAIChatCompletionModel.GPT_4_32k, - OpenAIChatCompletionModel.GPT_4, - OpenAIChatCompletionModel.GPT_3_5) + OpenAIChatCompletionModel.GPT_3_5_0125_16k) .forEach(model -> actionGroup.add(createOpenAIModelAction(model, presentation))); actionGroup.addSeparator("Custom OpenAI Service"); actionGroup.add(createModelAction( @@ -111,8 +113,12 @@ public class ModelComboBoxAction extends ComboBoxAction { Icons.Llama, presentation)); actionGroup.addSeparator("Ollama"); - ollamaSettings.getAvailableModels().forEach(model -> - actionGroup.add(createOllamaModelAction(model, presentation))); + ApplicationManager.getApplication() + .getService(OllamaSettings.class) + .getState() + .getAvailableModels() + .forEach(model -> + actionGroup.add(createOllamaModelAction(model, presentation))); actionGroup.addSeparator(); actionGroup.add(createModelAction( ServiceType.GOOGLE, @@ -160,20 +166,33 @@ public class ModelComboBoxAction extends ComboBoxAction { } private void updateTemplatePresentation(ServiceType selectedService) { + var application = ApplicationManager.getApplication(); var templatePresentation = getTemplatePresentation(); switch (selectedService) { + case CODEGPT: + var model = application.getService(CodeGPTServiceSettings.class) + .getState() + .getChatCompletionSettings() + .getModel(); + var modelName = AVAILABLE_CHAT_MODELS.stream() + .filter(it -> it.getCode().equals(model)) + .map(CodeGPTModel::getName) + .findFirst().orElse("Unknown"); + templatePresentation.setIcon(Icons.CodeGPTModel); + templatePresentation.setText(modelName); + break; case OPENAI: templatePresentation.setIcon(Icons.OpenAI); templatePresentation.setText( - OpenAIChatCompletionModel.findByCode(openAISettings.getModel()).getDescription()); + OpenAIChatCompletionModel.findByCode(OpenAISettings.getCurrentState().getModel()) + .getDescription()); break; case CUSTOM_OPENAI: templatePresentation.setIcon(Icons.OpenAI); - templatePresentation.setText( - ApplicationManager.getApplication().getService(CustomServiceSettings.class) - .getState() - .getTemplate() - .getProviderName()); + templatePresentation.setText(application.getService(CustomServiceSettings.class) + .getState() + .getTemplate() + .getProviderName()); break; case ANTHROPIC: templatePresentation.setIcon(Icons.Anthropic); @@ -184,11 +203,12 @@ public class ModelComboBoxAction extends ComboBoxAction { templatePresentation.setText("Azure OpenAI"); break; case YOU: + var settings = YouSettings.getCurrentState(); templatePresentation.setIcon(Icons.YouSmall); templatePresentation.setText( - youSettings.getChatMode() == YouCompletionMode.CUSTOM - ? youSettings.getCustomModel().getDescription() - : youSettings.getChatMode().getDescription() + settings.getChatMode() == YouCompletionMode.CUSTOM + ? settings.getCustomModel().getDescription() + : settings.getChatMode().getDescription() ); break; case LLAMA_CPP: @@ -197,7 +217,9 @@ public class ModelComboBoxAction extends ComboBoxAction { break; case OLLAMA: templatePresentation.setIcon(Icons.Ollama); - templatePresentation.setText(ollamaSettings.getModel()); + templatePresentation.setText(application.getService(OllamaSettings.class) + .getState() + .getModel()); break; case GOOGLE: templatePresentation.setText("Google (Gemini)"); @@ -254,12 +276,42 @@ public class ModelComboBoxAction extends ComboBoxAction { String label, Icon icon, Presentation comboBoxPresentation) { - settings.setSelectedService(serviceType); + GeneralSettings.getCurrentState().setSelectedService(serviceType); comboBoxPresentation.setIcon(icon); comboBoxPresentation.setText(label); onModelChange.run(); } + private AnAction createCodeGPTModelAction(CodeGPTModel model, boolean enabled, + Presentation comboBoxPresentation) { + return new DumbAwareAction(model.getName(), "", Icons.CodeGPTModel) { + @Override + public void update(@NotNull AnActionEvent event) { + var presentation = event.getPresentation(); + presentation.setEnabled( + enabled && !presentation.getText().equals(comboBoxPresentation.getText())); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + ApplicationManager.getApplication().getService(CodeGPTServiceSettings.class) + .getState() + .getChatCompletionSettings() + .setModel(model.getCode()); + handleModelChange( + CODEGPT, + model.getName(), + Icons.OpenAI, + comboBoxPresentation); + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.BGT; + } + }; + } + private AnAction createOllamaModelAction( String model, Presentation comboBoxPresentation @@ -273,7 +325,10 @@ public class ModelComboBoxAction extends ComboBoxAction { @Override public void actionPerformed(@NotNull AnActionEvent e) { - ollamaSettings.setModel(model); + ApplicationManager.getApplication() + .getService(OllamaSettings.class) + .getState() + .setModel(model); handleModelChange( OLLAMA, model, @@ -302,7 +357,7 @@ public class ModelComboBoxAction extends ComboBoxAction { @Override public void actionPerformed(@NotNull AnActionEvent e) { - openAISettings.setModel(model.getCode()); + OpenAISettings.getCurrentState().setModel(model.getCode()); handleModelChange( OPENAI, model.getDescription(), @@ -331,7 +386,7 @@ public class ModelComboBoxAction extends ComboBoxAction { @Override public void actionPerformed(@NotNull AnActionEvent e) { - youSettings.setChatMode(mode); + YouSettings.getCurrentState().setChatMode(mode); handleModelChange( YOU, mode.getDescription(), @@ -360,8 +415,9 @@ public class ModelComboBoxAction extends ComboBoxAction { @Override public void actionPerformed(@NotNull AnActionEvent e) { - youSettings.setCustomModel(model); - youSettings.setChatMode(YouCompletionMode.CUSTOM); + var settings = YouSettings.getCurrentState(); + settings.setCustomModel(model); + settings.setChatMode(YouCompletionMode.CUSTOM); handleModelChange( YOU, model.getDescription(), diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt index 121c4a54..6a0b29d0 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt @@ -4,9 +4,10 @@ import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent 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.ServiceType import ee.carlrobert.codegpt.settings.service.ServiceType.* +import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings @@ -18,6 +19,9 @@ abstract class CodeCompletionFeatureToggleActions( override fun actionPerformed(e: AnActionEvent) { when (GeneralSettings.getCurrentState().selectedService) { + CODEGPT -> + service().state.codeCompletionSettings.codeCompletionsEnabled + OPENAI -> OpenAISettings.getCurrentState().isCodeCompletionsEnabled = enableFeatureAction @@ -40,9 +44,11 @@ abstract class CodeCompletionFeatureToggleActions( override fun update(e: AnActionEvent) { val selectedService = GeneralSettings.getCurrentState().selectedService - val codeCompletionEnabled = isCodeCompletionsEnabled(selectedService) + val codeCompletionEnabled = + service().isCodeCompletionsEnabled(selectedService) e.presentation.isVisible = codeCompletionEnabled != enableFeatureAction e.presentation.isEnabled = when (selectedService) { + CODEGPT, OPENAI, CUSTOM_OPENAI, LLAMA_CPP, @@ -59,19 +65,6 @@ abstract class CodeCompletionFeatureToggleActions( override fun getActionUpdateThread(): ActionUpdateThread { return ActionUpdateThread.BGT } - - private fun isCodeCompletionsEnabled(serviceType: ServiceType): Boolean { - return when (serviceType) { - OPENAI -> OpenAISettings.getCurrentState().isCodeCompletionsEnabled - CUSTOM_OPENAI -> service().state.codeCompletionSettings.codeCompletionsEnabled - LLAMA_CPP -> LlamaSettings.isCodeCompletionsPossible() - OLLAMA -> service().state.codeCompletionsEnabled - ANTHROPIC, - AZURE, - GOOGLE, - YOU -> false - } - } } class EnableCompletionsAction : CodeCompletionFeatureToggleActions(true) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt index a743a76c..3fe627dc 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt @@ -7,6 +7,7 @@ import ee.carlrobert.codegpt.completions.llama.LlamaModel import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential import ee.carlrobert.codegpt.settings.configuration.Placeholder +import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettingsState @@ -23,6 +24,18 @@ import java.nio.charset.StandardCharsets object CodeCompletionRequestFactory { + @JvmStatic + fun buildCodeGPTRequest(details: InfillRequestDetails): OpenAITextCompletionRequest { + val settings = service().state.codeCompletionSettings + return OpenAITextCompletionRequest.Builder(details.prefix) + .setSuffix(details.suffix) + .setStream(true) + .setModel(settings.model) + .setMaxTokens(settings.maxTokens) + .setTemperature(0.4) + .build() + } + @JvmStatic fun buildOpenAIRequest(details: InfillRequestDetails): OpenAITextCompletionRequest { return OpenAITextCompletionRequest.Builder(details.prefix) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionService.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionService.kt new file mode 100644 index 00000000..a2093b31 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionService.kt @@ -0,0 +1,59 @@ +package ee.carlrobert.codegpt.codecompletions + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import ee.carlrobert.codegpt.codecompletions.CodeCompletionRequestFactory.buildCodeGPTRequest +import ee.carlrobert.codegpt.codecompletions.CodeCompletionRequestFactory.buildCustomRequest +import ee.carlrobert.codegpt.codecompletions.CodeCompletionRequestFactory.buildLlamaRequest +import ee.carlrobert.codegpt.codecompletions.CodeCompletionRequestFactory.buildOpenAIRequest +import ee.carlrobert.codegpt.completions.CompletionClientProvider +import ee.carlrobert.codegpt.settings.GeneralSettings +import ee.carlrobert.codegpt.settings.service.ServiceType +import ee.carlrobert.codegpt.settings.service.ServiceType.* +import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings +import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings +import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings +import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings +import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings +import ee.carlrobert.llm.client.openai.completion.OpenAITextCompletionEventSourceListener +import ee.carlrobert.llm.completion.CompletionEventListener +import okhttp3.sse.EventSource +import okhttp3.sse.EventSources.createFactory + +@Service(Service.Level.PROJECT) +class CodeCompletionService { + + fun isCodeCompletionsEnabled(selectedService: ServiceType): Boolean = + when (selectedService) { + CODEGPT -> service().state.codeCompletionSettings.codeCompletionsEnabled + OPENAI -> OpenAISettings.getCurrentState().isCodeCompletionsEnabled + CUSTOM_OPENAI -> service().state.codeCompletionSettings.codeCompletionsEnabled + LLAMA_CPP -> LlamaSettings.isCodeCompletionsPossible() + OLLAMA -> service().state.codeCompletionsEnabled + else -> false + } + + fun getCodeCompletionAsync( + requestDetails: InfillRequestDetails, + eventListener: CompletionEventListener + ): EventSource = + when (val selectedService = GeneralSettings.getCurrentState().selectedService) { + CODEGPT -> CompletionClientProvider.getCodeGPTClient() + .getCompletionAsync(buildCodeGPTRequest(requestDetails), eventListener) + + OPENAI -> CompletionClientProvider.getOpenAIClient() + .getCompletionAsync(buildOpenAIRequest(requestDetails), eventListener) + + CUSTOM_OPENAI -> createFactory( + CompletionClientProvider.getDefaultClientBuilder().build() + ).newEventSource( + buildCustomRequest(requestDetails), + OpenAITextCompletionEventSourceListener(eventListener) + ) + + LLAMA_CPP -> CompletionClientProvider.getLlamaClient() + .getChatCompletionAsync(buildLlamaRequest(requestDetails), eventListener) + + else -> throw IllegalArgumentException("Code completion not supported for ${selectedService.name}") + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt index 9e632510..04895b3f 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt @@ -7,9 +7,9 @@ import com.intellij.openapi.application.EDT import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.thisLogger import ee.carlrobert.codegpt.CodeGPTKeys -import ee.carlrobert.codegpt.completions.CompletionRequestService import ee.carlrobert.codegpt.settings.GeneralSettings import ee.carlrobert.codegpt.settings.service.ServiceType +import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings @@ -36,7 +36,8 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { get() = InlineCompletionProviderID("CodeGPTInlineCompletionProvider") override suspend fun getSuggestion(request: InlineCompletionRequest): InlineCompletionSuggestion { - if (request.editor.project == null) { + val project = request.editor.project + if (project == null) { logger.error("Could not find project") return InlineCompletionSuggestion.empty() } @@ -46,7 +47,7 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { InfillRequestDetails.fromInlineCompletionRequest(request) } currentCall.set( - CompletionRequestService.getInstance().getCodeCompletionAsync( + project.service().getCodeCompletionAsync( infillRequest, CodeCompletionEventListener { val inlineText = it.takeWhile { message -> message != '\n' }.toString() @@ -68,6 +69,7 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { override fun isEnabled(event: InlineCompletionEvent): Boolean { val selectedService = GeneralSettings.getCurrentState().selectedService val codeCompletionsEnabled = when (selectedService) { + ServiceType.CODEGPT -> service().state.codeCompletionSettings.codeCompletionsEnabled ServiceType.OPENAI -> OpenAISettings.getCurrentState().isCodeCompletionsEnabled ServiceType.CUSTOM_OPENAI -> service().state.codeCompletionSettings.codeCompletionsEnabled ServiceType.LLAMA_CPP -> LlamaSettings.getCurrentState().isCodeCompletionsEnabled diff --git a/src/main/kotlin/ee/carlrobert/codegpt/credentials/CredentialsStore.kt b/src/main/kotlin/ee/carlrobert/codegpt/credentials/CredentialsStore.kt index de6f5055..72233af6 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/credentials/CredentialsStore.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/credentials/CredentialsStore.kt @@ -19,6 +19,7 @@ object CredentialsStore { } } + @JvmStatic fun getCredential(key: CredentialKey): String? = credentialsMap[key] fun setCredential(key: CredentialKey, password: String?) { @@ -26,7 +27,8 @@ object CredentialsStore { credentialsMap[key] = password if (prevPassword != password) { - val credentialAttributes = CredentialAttributes(generateServiceName("CodeGPT", key.name)) + val credentialAttributes = + CredentialAttributes(generateServiceName("CodeGPT", key.name)) PasswordSafe.instance.setPassword(credentialAttributes, password) } } @@ -34,6 +36,7 @@ object CredentialsStore { fun isCredentialSet(key: CredentialKey): Boolean = !getCredential(key).isNullOrEmpty() enum class CredentialKey { + CODEGPT_API_KEY, OPENAI_API_KEY, CUSTOM_SERVICE_API_KEY, ANTHROPIC_API_KEY, diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceForm.kt new file mode 100644 index 00000000..3336dc70 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceForm.kt @@ -0,0 +1,141 @@ +package ee.carlrobert.codegpt.settings.service.codegpt + +import com.intellij.openapi.components.service +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.panel.ComponentPanelBuilder +import com.intellij.ui.TitledSeparator +import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.JBPasswordField +import com.intellij.ui.components.fields.IntegerField +import com.intellij.util.ui.FormBuilder +import ee.carlrobert.codegpt.CodeGPTBundle +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CODEGPT_API_KEY +import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential +import ee.carlrobert.codegpt.credentials.CredentialsStore.setCredential +import ee.carlrobert.codegpt.ui.UIUtil +import ee.carlrobert.llm.client.codegpt.CodeGPTAvailableModels +import ee.carlrobert.llm.client.codegpt.CodeGPTAvailableModels.AVAILABLE_CHAT_MODELS +import ee.carlrobert.llm.client.codegpt.CodeGPTAvailableModels.AVAILABLE_CODE_MODELS +import ee.carlrobert.llm.client.codegpt.CodeGPTModel +import org.jdesktop.swingx.combobox.ListComboBoxModel +import java.awt.Component +import javax.swing.DefaultListCellRenderer +import javax.swing.JList +import javax.swing.JPanel + +class CodeGPTServiceForm { + + private val apiKeyField = JBPasswordField().apply { + columns = 30 + text = getCredential(CredentialKey.CUSTOM_SERVICE_API_KEY) + } + + private val chatCompletionModelComboBox = + ComboBox(ListComboBoxModel(AVAILABLE_CHAT_MODELS)).apply { + selectedItem = + CodeGPTAvailableModels.findByCode(service().state.chatCompletionSettings.model) + renderer = CustomComboBoxRenderer() + } + + private val codeCompletionsEnabledCheckBox = JBCheckBox( + CodeGPTBundle.get("codeCompletionsForm.enableFeatureText"), + service().state.codeCompletionSettings.codeCompletionsEnabled + ) + + private val codeCompletionModelComboBox = + ComboBox(ListComboBoxModel(AVAILABLE_CODE_MODELS)).apply { + selectedItem = + CodeGPTAvailableModels.findByCode(service().state.codeCompletionSettings.model) + renderer = CustomComboBoxRenderer() + } + + private val codeCompletionMaxTokensField = + IntegerField("completion_max_tokens", 8, 4096).apply { + columns = 12 + value = service().state.codeCompletionSettings.maxTokens + } + + fun getForm(): JPanel = FormBuilder.createFormBuilder() + .addComponent(TitledSeparator(CodeGPTBundle.get("shared.configuration"))) + .addComponent( + FormBuilder.createFormBuilder() + .setFormLeftIndent(16) + .addLabeledComponent( + CodeGPTBundle.get("settingsConfigurable.shared.apiKey.label"), + apiKeyField + ) + .addComponentToRightColumn( + UIUtil.createComment("settingsConfigurable.service.codegpt.apiKey.comment") + ) + .addLabeledComponent("Model:", chatCompletionModelComboBox) + .addVerticalGap(4) + .panel + ) + .addComponent(TitledSeparator("Code Completions")) + .addComponent( + FormBuilder.createFormBuilder() + .setFormLeftIndent(16) + .addComponent(codeCompletionsEnabledCheckBox) + .addLabeledComponent("Model:", codeCompletionModelComboBox) + .addLabeledComponent("Max tokens:", codeCompletionMaxTokensField) + .addComponentToRightColumn( + ComponentPanelBuilder.createCommentComponent( + CodeGPTBundle.get("codeCompletionsForm.maxTokensComment"), true, 48, true + ) + ) + .panel + ) + .panel + + fun getApiKey() = String(apiKeyField.password).ifEmpty { null } + + fun isModified() = service().state.run { + (chatCompletionModelComboBox.selectedItem as CodeGPTModel).code != chatCompletionSettings.model + || (codeCompletionModelComboBox.selectedItem as CodeGPTModel).code != codeCompletionSettings.model + || codeCompletionMaxTokensField.value != codeCompletionSettings.maxTokens + || codeCompletionsEnabledCheckBox.isSelected != codeCompletionSettings.codeCompletionsEnabled + || getApiKey() != getCredential(CODEGPT_API_KEY) + } + + fun applyChanges() { + service().state.run { + chatCompletionSettings.model = + (chatCompletionModelComboBox.selectedItem as CodeGPTModel).code + codeCompletionSettings.codeCompletionsEnabled = + codeCompletionsEnabledCheckBox.isSelected + codeCompletionSettings.maxTokens = codeCompletionMaxTokensField.value + codeCompletionSettings.model = + (codeCompletionModelComboBox.selectedItem as CodeGPTModel).code + } + setCredential(CODEGPT_API_KEY, getApiKey()) + } + + fun resetForm() { + service().state.run { + chatCompletionModelComboBox.selectedItem = chatCompletionSettings.model + codeCompletionModelComboBox.selectedItem = codeCompletionSettings.model + codeCompletionMaxTokensField.value = codeCompletionSettings.maxTokens + codeCompletionsEnabledCheckBox.isSelected = + codeCompletionSettings.codeCompletionsEnabled + } + apiKeyField.text = getCredential(CODEGPT_API_KEY) + } + + private class CustomComboBoxRenderer : DefaultListCellRenderer() { + override fun getListCellRendererComponent( + list: JList<*>, + value: Any?, + index: Int, + isSelected: Boolean, + cellHasFocus: Boolean + ): Component { + val component = + super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus) + if (value is CodeGPTModel) { + text = value.name + } + return component + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceSettings.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceSettings.kt new file mode 100644 index 00000000..7aa9bd62 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceSettings.kt @@ -0,0 +1,26 @@ +package ee.carlrobert.codegpt.settings.service.codegpt + +import com.intellij.openapi.components.* + +@Service +@State( + name = "CodeGPT_CodeGPTServiceSettings", + storages = [Storage("CodeGPT_CodeGPTServiceSettings.xml")] +) +class CodeGPTServiceSettings : + SimplePersistentStateComponent(CodeGPTServiceSettingsState()) + +class CodeGPTServiceSettingsState : BaseState() { + var chatCompletionSettings by property(CodeGPTServiceChatCompletionSettingsState()) + var codeCompletionSettings by property(CodeGPTServiceCodeCompletionSettingsState()) +} + +class CodeGPTServiceChatCompletionSettingsState : BaseState() { + var model by string("meta-llama/Llama-3-70b-chat-hf") +} + +class CodeGPTServiceCodeCompletionSettingsState : BaseState() { + var codeCompletionsEnabled by property(true) + var model by string("codellama/CodeLlama-70b-hf") + var maxTokens by property(128) +} diff --git a/src/main/resources/icons/codegpt-model.svg b/src/main/resources/icons/codegpt-model.svg new file mode 100644 index 00000000..89919566 --- /dev/null +++ b/src/main/resources/icons/codegpt-model.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/main/resources/icons/codegpt-model_dark.svg b/src/main/resources/icons/codegpt-model_dark.svg new file mode 100644 index 00000000..00581547 --- /dev/null +++ b/src/main/resources/icons/codegpt-model_dark.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index 04b1d7e5..3ad8afcb 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -18,6 +18,7 @@ settings.displayName=CodeGPT: Settings settings.openaiQuotaExceeded=OpenAI quota exceeded. settingsConfigurable.displayName.label=Display name: settingsConfigurable.service.label=Service: +settingsConfigurable.service.codegpt.apiKey.comment=You can find the API key in your User settings. settingsConfigurable.service.custom.openai.apiKey.comment=A secret value stored in the system's Keychain or KeePass, depending on your OS. This approach is recommended over storing the secret in the header as plain text. settingsConfigurable.service.openai.apiKey.comment=You can find the API key in your User settings. settingsConfigurable.service.openai.customModel.label=Custom model: @@ -170,6 +171,7 @@ toolwindow.chat.youProCheckBox.enable=Turn on for complex queries toolwindow.chat.youProCheckBox.disable=Turn off for faster responses toolwindow.chat.youProCheckBox.notAllowed=Enable by subscribing to YouPro plan toolwindow.chat.textArea.emptyText=Ask me anything... +service.codegpt.title=CodeGPT Service service.openai.title=OpenAI Service service.custom.openai.title=Custom OpenAI Service service.anthropic.title=Anthropic Service @@ -209,4 +211,4 @@ shared.chatCompletions=Chat Completions shared.codeCompletions=Code Completions codeCompletionsForm.enableFeatureText=Enable code completions codeCompletionsForm.maxTokensLabel=Max tokens: -codeCompletionsForm.maxTokensComment=The maximum number of tokens that can be generated in the code completion. +codeCompletionsForm.maxTokensComment=The maximum number of tokens that will be generated in the code completion. \ No newline at end of file diff --git a/src/test/kotlin/ee/carlrobert/codegpt/completions/CompletionRequestProviderTest.kt b/src/test/kotlin/ee/carlrobert/codegpt/completions/CompletionRequestProviderTest.kt index ef874ecf..bb18c9c4 100644 --- a/src/test/kotlin/ee/carlrobert/codegpt/completions/CompletionRequestProviderTest.kt +++ b/src/test/kotlin/ee/carlrobert/codegpt/completions/CompletionRequestProviderTest.kt @@ -3,8 +3,6 @@ package ee.carlrobert.codegpt.completions import ee.carlrobert.codegpt.completions.CompletionRequestProvider.COMPLETION_SYSTEM_PROMPT import ee.carlrobert.codegpt.conversations.ConversationService import ee.carlrobert.codegpt.conversations.message.Message -import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey -import ee.carlrobert.codegpt.credentials.CredentialsStore.setCredential import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel import org.assertj.core.api.Assertions.assertThat @@ -14,7 +12,7 @@ import testsupport.IntegrationTest class CompletionRequestProviderTest : IntegrationTest() { fun testChatCompletionRequestWithSystemPromptOverride() { - setCredential(CredentialKey.OPENAI_API_KEY, "TEST_API_KEY") + useOpenAIService() ConfigurationSettings.getCurrentState().systemPrompt = "TEST_SYSTEM_PROMPT" val conversation = ConversationService.getInstance().startConversation() val firstMessage = createDummyMessage(500) @@ -43,6 +41,7 @@ class CompletionRequestProviderTest : IntegrationTest() { } fun testChatCompletionRequestWithoutSystemPromptOverride() { + useOpenAIService() ConfigurationSettings.getCurrentState().systemPrompt = COMPLETION_SYSTEM_PROMPT val conversation = ConversationService.getInstance().startConversation() val firstMessage = createDummyMessage(500) @@ -71,6 +70,7 @@ class CompletionRequestProviderTest : IntegrationTest() { } fun testChatCompletionRequestRetry() { + useOpenAIService() ConfigurationSettings.getCurrentState().systemPrompt = "TEST_SYSTEM_PROMPT" val conversation = ConversationService.getInstance().startConversation() val firstMessage = createDummyMessage("FIRST_TEST_PROMPT", 500) @@ -97,6 +97,7 @@ class CompletionRequestProviderTest : IntegrationTest() { } fun testReducedChatCompletionRequest() { + useOpenAIService() ConfigurationSettings.getCurrentState().systemPrompt = COMPLETION_SYSTEM_PROMPT val conversation = ConversationService.getInstance().startConversation() conversation.addMessage(createDummyMessage(50)) @@ -126,6 +127,7 @@ class CompletionRequestProviderTest : IntegrationTest() { } fun testTotalUsageExceededException() { + useOpenAIService() val conversation = ConversationService.getInstance().startConversation() conversation.addMessage(createDummyMessage(1500)) conversation.addMessage(createDummyMessage(1500)) diff --git a/src/test/kotlin/ee/carlrobert/codegpt/completions/DefaultCompletionRequestHandlerTest.kt b/src/test/kotlin/ee/carlrobert/codegpt/completions/DefaultCompletionRequestHandlerTest.kt index 9cbbdea7..5e99ec2a 100644 --- a/src/test/kotlin/ee/carlrobert/codegpt/completions/DefaultCompletionRequestHandlerTest.kt +++ b/src/test/kotlin/ee/carlrobert/codegpt/completions/DefaultCompletionRequestHandlerTest.kt @@ -205,6 +205,37 @@ class DefaultCompletionRequestHandlerTest : IntegrationTest() { waitExpecting { "Hello!" == message.response } } + fun testCodeGPTServiceChatCompletionCall() { + useCodeGPTService() + ConfigurationSettings.getCurrentState().systemPrompt = "TEST_SYSTEM_PROMPT" + val message = Message("TEST_PROMPT") + val conversation = ConversationService.getInstance().startConversation() + val requestHandler = CompletionRequestHandler(getRequestEventListener(message)) + expectCodeGPT(StreamHttpExchange { request: RequestEntity -> + assertThat(request.uri.path).isEqualTo("/v1/chat/completions") + assertThat(request.method).isEqualTo("POST") + assertThat(request.headers[HttpHeaders.AUTHORIZATION]!![0]).isEqualTo("Bearer TEST_API_KEY") + assertThat(request.body) + .extracting( + "model", + "messages") + .containsExactly( + "TEST_MODEL", + listOf( + mapOf("role" to "system", "content" to "TEST_SYSTEM_PROMPT"), + mapOf("role" to "user", "content" to "TEST_PROMPT"))) + listOf( + jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("role", "assistant")))), + jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "Hel")))), + jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "lo")))), + jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "!"))))) + }) + + requestHandler.call(CallParameters(conversation, ConversationType.DEFAULT, message, false)) + + waitExpecting { "Hello!" == message.response } + } + private fun getRequestEventListener(message: Message): CompletionResponseEventListener { return object : CompletionResponseEventListener { override fun handleCompleted(fullMessage: String, callParameters: CallParameters) { diff --git a/src/test/kotlin/testsupport/mixin/ShortcutsTestMixin.kt b/src/test/kotlin/testsupport/mixin/ShortcutsTestMixin.kt index 2ce62938..2273ca08 100644 --- a/src/test/kotlin/testsupport/mixin/ShortcutsTestMixin.kt +++ b/src/test/kotlin/testsupport/mixin/ShortcutsTestMixin.kt @@ -7,6 +7,7 @@ import ee.carlrobert.codegpt.credentials.CredentialsStore.setCredential import ee.carlrobert.codegpt.settings.GeneralSettings import ee.carlrobert.codegpt.settings.service.ServiceType import ee.carlrobert.codegpt.settings.service.azure.AzureSettings +import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings import ee.carlrobert.codegpt.settings.service.google.GoogleSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings @@ -15,6 +16,12 @@ import java.util.function.BooleanSupplier interface ShortcutsTestMixin { + fun useCodeGPTService() { + GeneralSettings.getCurrentState().selectedService = ServiceType.CODEGPT + setCredential(CODEGPT_API_KEY, "TEST_API_KEY") + service().state.chatCompletionSettings.model = "TEST_MODEL" + } + fun useOpenAIService() { useOpenAIService("gpt-4") } @@ -49,7 +56,6 @@ interface ShortcutsTestMixin { service().state.model = GoogleModel.GEMINI_PRO.code } - fun waitExpecting(condition: BooleanSupplier?) { PlatformTestUtil.waitWithEventsDispatching( "Waiting for message response timed out or did not meet expected conditions", @@ -57,5 +63,4 @@ interface ShortcutsTestMixin { 5 ) } - }