From 14f325491340b3ec3b8b3e8b9213877b1446bf56 Mon Sep 17 00:00:00 2001 From: Simon Svensson Date: Sat, 20 Apr 2024 22:23:08 +0200 Subject: [PATCH] feat: code completion for "Custom OpenAI Service" (#476) * Add code completion setting states for custom service * Add settings for code completion in Custom OpenAI service * Move code completion section to the bottom * Create test testFetchCodeCompletionCustomService * Add Custom OpenAI to the "Enable/Disable Completion" actions * New configuration UI separating /v1/chat/completions from /v1/completions * Code completion for Custom Service * Formatting fixes * Move prefix and suffix to templates in body * Message updates * New tabbed UI for Chat and Code Completions * convert to kotlin, improve ui and other minor changes * fix test connection for chat completions * add help tooltips * allow backward compatibility * support prefix and suffix placeholders * fix initial state loading --------- Co-authored-by: Jack Boswell (boswelja) Co-authored-by: Carl-Robert Linnupuu --- README.md | 2 +- .../CompletionRequestProvider.java | 31 ++- .../completions/CompletionRequestService.java | 23 ++- .../settings/GeneralSettingsComponent.java | 78 ++++++-- .../settings/GeneralSettingsConfigurable.java | 34 ++-- .../service/ServiceSelectionForm.java | 68 ------- .../service/custom/CustomServiceForm.java | 175 ----------------- .../custom/CustomServiceFormTabbedPane.java | 11 +- .../service/custom/CustomServiceSettings.java | 45 ----- .../custom/CustomServiceSettingsState.java | 69 ------- .../service/custom/CustomServiceTemplate.java | 158 --------------- .../chat/ui/textarea/ModelComboBoxAction.java | 13 +- .../CodeCompletionFeatureToggleActions.kt | 14 +- .../CodeCompletionRequestFactory.kt | 56 ++++++ .../CodeGPTInlineCompletionProvider.kt | 22 ++- .../codecompletions/InfillRequestDetails.kt | 10 +- .../settings/configuration/Placeholder.kt | 5 +- .../custom/CustomServiceChatCompletionForm.kt | 101 ++++++++++ .../CustomServiceChatCompletionTemplate.kt | 124 ++++++++++++ .../custom/CustomServiceCodeCompletionForm.kt | 182 ++++++++++++++++++ .../CustomServiceCodeCompletionTemplate.kt | 73 +++++++ .../service/custom/CustomServiceForm.kt | 148 ++++++++++++++ .../service/custom/CustomServiceSettings.kt | 74 +++++++ .../service/custom/CustomServiceTemplate.kt | 69 +++++++ src/main/resources/META-INF/plugin.xml | 5 +- .../resources/messages/codegpt.properties | 3 +- 26 files changed, 1001 insertions(+), 592 deletions(-) delete mode 100644 src/main/java/ee/carlrobert/codegpt/settings/service/ServiceSelectionForm.java delete mode 100644 src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceForm.java delete mode 100644 src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.java delete mode 100644 src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettingsState.java delete mode 100644 src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceTemplate.java create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceChatCompletionForm.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceChatCompletionTemplate.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceCodeCompletionForm.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceCodeCompletionTemplate.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceForm.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceTemplate.kt diff --git a/README.md b/README.md index a9635aa09..033bc4855 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Receive single-line or whole-function autocomplete suggestions as you type. ![Code Completions](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/inline-completion.png?raw=true) -> **Note**: Currently supported only on GPT-3.5 and locally-hosted models. +> **Note**: Currently only supported with OpenAI, Custom OpenAI, or LLaMA. ### Chat (with Vision) diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java index a4cde93bf..160d71893 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java @@ -4,6 +4,7 @@ import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CUSTOM_SERVICE_API_KEY; import static ee.carlrobert.codegpt.util.file.FileUtil.getResourceContent; import static java.lang.String.format; +import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; @@ -23,8 +24,9 @@ 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.custom.CustomServiceChatCompletionSettingsState; import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings; -import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettingsState; +import ee.carlrobert.codegpt.settings.service.custom.CustomServiceState; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; import ee.carlrobert.codegpt.settings.service.you.YouSettings; @@ -114,16 +116,27 @@ public static OpenAIChatCompletionRequest buildOpenAILookupCompletionRequest(Str public static Request buildCustomOpenAICompletionRequest(String system, String context) { return buildCustomOpenAIChatCompletionRequest( - CustomServiceSettings.getCurrentState(), + ApplicationManager.getApplication().getService(CustomServiceState.class) + .getChatCompletionSettings(), List.of( new OpenAIChatCompletionStandardMessage("system", system), new OpenAIChatCompletionStandardMessage("user", context)), true); } + public static Request buildCustomOpenAICompletionRequest(String input) { + return buildCustomOpenAIChatCompletionRequest( + ApplicationManager.getApplication().getService(CustomServiceSettings.class) + .getState() + .getChatCompletionSettings(), + List.of(new OpenAIChatCompletionStandardMessage("user", input)), + true); + } + public static Request buildCustomOpenAILookupCompletionRequest(String context) { return buildCustomOpenAIChatCompletionRequest( - CustomServiceSettings.getCurrentState(), + ApplicationManager.getApplication().getService(CustomServiceState.class) + .getChatCompletionSettings(), List.of( new OpenAIChatCompletionStandardMessage( "system", @@ -199,21 +212,21 @@ public OpenAIChatCompletionRequest buildOpenAIChatCompletionRequest( } public Request buildCustomOpenAIChatCompletionRequest( - CustomServiceSettingsState customConfiguration, + CustomServiceChatCompletionSettingsState settings, CallParameters callParameters) { return buildCustomOpenAIChatCompletionRequest( - customConfiguration, + settings, buildMessages(callParameters), true); } private static Request buildCustomOpenAIChatCompletionRequest( - CustomServiceSettingsState customConfiguration, + CustomServiceChatCompletionSettingsState settings, List messages, boolean streamRequest) { - var requestBuilder = new Request.Builder().url(customConfiguration.getUrl().trim()); + var requestBuilder = new Request.Builder().url(requireNonNull(settings.getUrl()).trim()); var credential = CredentialsStore.INSTANCE.getCredential(CUSTOM_SERVICE_API_KEY); - for (var entry : customConfiguration.getHeaders().entrySet()) { + for (var entry : settings.getHeaders().entrySet()) { String value = entry.getValue(); if (credential != null && value.contains("$CUSTOM_SERVICE_API_KEY")) { value = value.replace("$CUSTOM_SERVICE_API_KEY", credential); @@ -221,7 +234,7 @@ private static Request buildCustomOpenAIChatCompletionRequest( requestBuilder.addHeader(entry.getKey(), value); } - var body = customConfiguration.getBody().entrySet().stream() + var body = settings.getBody().entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, entry -> { diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java index 6f34310ff..edacf6552 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java @@ -29,6 +29,7 @@ import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionStandardMessage; import ee.carlrobert.llm.client.llama.completion.LlamaCompletionRequest; import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionEventSourceListener; +import ee.carlrobert.llm.client.openai.completion.OpenAITextCompletionEventSourceListener; import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionRequest; import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionStandardMessage; import ee.carlrobert.llm.client.openai.completion.response.OpenAIChatCompletionResponse; @@ -55,6 +56,15 @@ public static CompletionRequestService getInstance() { return ApplicationManager.getApplication().getService(CompletionRequestService.class); } + public EventSource getCustomOpenAICompletionAsync( + Request customRequest, + CompletionEventListener eventListener) { + var httpClient = CompletionClientProvider.getDefaultClientBuilder().build(); + return EventSources.createFactory(httpClient).newEventSource( + customRequest, + new OpenAITextCompletionEventSourceListener(eventListener)); + } + public EventSource getCustomOpenAIChatCompletionAsync( Request customRequest, CompletionEventListener eventListener) { @@ -76,7 +86,10 @@ public EventSource getChatCompletionAsync( eventListener); case CUSTOM_OPENAI -> getCustomOpenAIChatCompletionAsync( requestProvider.buildCustomOpenAIChatCompletionRequest( - CustomServiceSettings.getCurrentState(), + ApplicationManager.getApplication() + .getService(CustomServiceSettings.class) + .getState() + .getChatCompletionSettings(), callParameters), eventListener); case ANTHROPIC -> CompletionClientProvider.getClaudeClient().getCompletionAsync( @@ -99,14 +112,18 @@ public EventSource getChatCompletionAsync( public EventSource getCodeCompletionAsync( InfillRequestDetails requestDetails, CompletionEventListener eventListener) { + var httpClient = CompletionClientProvider.getDefaultClientBuilder().build(); return switch (GeneralSettings.getCurrentState().getSelectedService()) { case OPENAI -> CompletionClientProvider.getOpenAIClient() .getCompletionAsync( - CodeCompletionRequestFactory.INSTANCE.buildOpenAIRequest(requestDetails), + CodeCompletionRequestFactory.buildOpenAIRequest(requestDetails), eventListener); + case CUSTOM_OPENAI -> EventSources.createFactory(httpClient).newEventSource( + CodeCompletionRequestFactory.buildCustomRequest(requestDetails), + new OpenAITextCompletionEventSourceListener(eventListener)); case LLAMA_CPP -> CompletionClientProvider.getLlamaClient() .getChatCompletionAsync( - CodeCompletionRequestFactory.INSTANCE.buildLlamaRequest(requestDetails), + CodeCompletionRequestFactory.buildLlamaRequest(requestDetails), eventListener); default -> throw new IllegalArgumentException("Code completion not supported for selected service"); diff --git a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsComponent.java b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsComponent.java index 74377c2ec..d5fea9190 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsComponent.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsComponent.java @@ -12,8 +12,18 @@ import com.intellij.ui.components.JBTextField; import com.intellij.util.ui.FormBuilder; import ee.carlrobert.codegpt.CodeGPTBundle; -import ee.carlrobert.codegpt.settings.service.ServiceSelectionForm; import ee.carlrobert.codegpt.settings.service.ServiceType; +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.custom.CustomServiceForm; +import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; +import ee.carlrobert.codegpt.settings.service.llama.form.LlamaSettingsForm; +import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; +import ee.carlrobert.codegpt.settings.service.openai.OpenAISettingsForm; +import ee.carlrobert.codegpt.settings.service.you.YouSettings; +import ee.carlrobert.codegpt.settings.service.you.YouSettingsForm; import java.awt.CardLayout; import java.awt.Component; import java.awt.Container; @@ -29,21 +39,30 @@ public class GeneralSettingsComponent { private final JPanel mainPanel; private final JBTextField displayNameField; private final ComboBox serviceComboBox; - private final ServiceSelectionForm serviceSelectionForm; + private final OpenAISettingsForm openAISettingsForm; + private final CustomServiceForm customConfigurationSettingsForm; + private final AnthropicSettingsForm anthropicSettingsForm; + private final AzureSettingsForm azureSettingsForm; + private final YouSettingsForm youSettingsForm; + private final LlamaSettingsForm llamaSettingsForm; public GeneralSettingsComponent(Disposable parentDisposable, GeneralSettings settings) { displayNameField = new JBTextField(settings.getState().getDisplayName(), 20); - serviceSelectionForm = new ServiceSelectionForm(parentDisposable); + openAISettingsForm = new OpenAISettingsForm(OpenAISettings.getCurrentState()); + customConfigurationSettingsForm = new CustomServiceForm(); + anthropicSettingsForm = new AnthropicSettingsForm(AnthropicSettings.getCurrentState()); + azureSettingsForm = new AzureSettingsForm(AzureSettings.getCurrentState()); + youSettingsForm = new YouSettingsForm(YouSettings.getCurrentState(), parentDisposable); + llamaSettingsForm = new LlamaSettingsForm(LlamaSettings.getCurrentState()); + var cardLayout = new DynamicCardLayout(); var cards = new JPanel(cardLayout); - cards.add(serviceSelectionForm.getOpenAISettingsForm().getForm(), OPENAI.getCode()); - cards.add( - serviceSelectionForm.getCustomConfigurationSettingsForm().getForm(), - CUSTOM_OPENAI.getCode()); - cards.add(serviceSelectionForm.getAnthropicSettingsForm().getForm(), ANTHROPIC.getCode()); - cards.add(serviceSelectionForm.getAzureSettingsForm().getForm(), AZURE.getCode()); - cards.add(serviceSelectionForm.getYouSettingsForm(), YOU.getCode()); - cards.add(serviceSelectionForm.getLlamaSettingsForm(), LLAMA_CPP.getCode()); + cards.add(openAISettingsForm.getForm(), OPENAI.getCode()); + cards.add(customConfigurationSettingsForm.getForm(), CUSTOM_OPENAI.getCode()); + cards.add(anthropicSettingsForm.getForm(), ANTHROPIC.getCode()); + cards.add(azureSettingsForm.getForm(), AZURE.getCode()); + cards.add(youSettingsForm, YOU.getCode()); + cards.add(llamaSettingsForm, LLAMA_CPP.getCode()); var serviceComboBoxModel = new DefaultComboBoxModel(); serviceComboBoxModel.addAll(Arrays.stream(ServiceType.values()).toList()); serviceComboBox = new ComboBox<>(serviceComboBoxModel); @@ -63,6 +82,30 @@ public GeneralSettingsComponent(Disposable parentDisposable, GeneralSettings set .getPanel(); } + public OpenAISettingsForm getOpenAISettingsForm() { + return openAISettingsForm; + } + + public CustomServiceForm getCustomConfigurationSettingsForm() { + return customConfigurationSettingsForm; + } + + public AnthropicSettingsForm getAnthropicSettingsForm() { + return anthropicSettingsForm; + } + + public AzureSettingsForm getAzureSettingsForm() { + return azureSettingsForm; + } + + public LlamaSettingsForm getLlamaSettingsForm() { + return llamaSettingsForm; + } + + public YouSettingsForm getYouSettingsForm() { + return youSettingsForm; + } + public ServiceType getSelectedService() { return serviceComboBox.getItem(); } @@ -79,10 +122,6 @@ public JComponent getPreferredFocusedComponent() { return displayNameField; } - public ServiceSelectionForm getServiceSelectionForm() { - return serviceSelectionForm; - } - public String getDisplayName() { return displayNameField.getText(); } @@ -91,6 +130,15 @@ public void setDisplayName(String displayName) { displayNameField.setText(displayName); } + public void resetForms() { + openAISettingsForm.resetForm(); + customConfigurationSettingsForm.resetForm(); + anthropicSettingsForm.resetForm(); + azureSettingsForm.resetForm(); + youSettingsForm.resetForm(); + llamaSettingsForm.resetForm(); + } + static class DynamicCardLayout extends CardLayout { @Override diff --git a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java index b9d781bc4..3d12089a9 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java @@ -18,7 +18,6 @@ import ee.carlrobert.codegpt.settings.service.azure.AzureSettings; import ee.carlrobert.codegpt.settings.service.azure.AzureSettingsForm; import ee.carlrobert.codegpt.settings.service.custom.CustomServiceForm; -import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.llama.form.LlamaSettingsForm; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; @@ -61,17 +60,15 @@ public JComponent createComponent() { @Override public boolean isModified() { var settings = GeneralSettings.getCurrentState(); - var serviceSelectionForm = component.getServiceSelectionForm(); + return !component.getDisplayName().equals(settings.getDisplayName()) || component.getSelectedService() != settings.getSelectedService() - || OpenAISettings.getInstance().isModified(serviceSelectionForm.getOpenAISettingsForm()) - || CustomServiceSettings.getInstance() - .isModified(serviceSelectionForm.getCustomConfigurationSettingsForm()) - || AnthropicSettings.getInstance() - .isModified(serviceSelectionForm.getAnthropicSettingsForm()) - || AzureSettings.getInstance().isModified(serviceSelectionForm.getAzureSettingsForm()) - || YouSettings.getInstance().isModified(serviceSelectionForm.getYouSettingsForm()) - || LlamaSettings.getInstance().isModified(serviceSelectionForm.getLlamaSettingsForm()); + || OpenAISettings.getInstance().isModified(component.getOpenAISettingsForm()) + || component.getCustomConfigurationSettingsForm().isModified() + || AnthropicSettings.getInstance().isModified(component.getAnthropicSettingsForm()) + || AzureSettings.getInstance().isModified(component.getAzureSettingsForm()) + || YouSettings.getInstance().isModified(component.getYouSettingsForm()) + || LlamaSettings.getInstance().isModified(component.getLlamaSettingsForm()); } @Override @@ -80,14 +77,13 @@ public void apply() { settings.setDisplayName(component.getDisplayName()); settings.setSelectedService(component.getSelectedService()); - var serviceSelectionForm = component.getServiceSelectionForm(); - var openAISettingsForm = serviceSelectionForm.getOpenAISettingsForm(); + var openAISettingsForm = component.getOpenAISettingsForm(); applyOpenAISettings(openAISettingsForm); - applyCustomOpenAISettings(serviceSelectionForm.getCustomConfigurationSettingsForm()); - applyAnthropicSettings(serviceSelectionForm.getAnthropicSettingsForm()); - applyAzureSettings(serviceSelectionForm.getAzureSettingsForm()); - applyYouSettings(serviceSelectionForm.getYouSettingsForm()); - applyLlamaSettings(serviceSelectionForm.getLlamaSettingsForm()); + applyCustomOpenAISettings(component.getCustomConfigurationSettingsForm()); + applyAnthropicSettings(component.getAnthropicSettingsForm()); + applyAzureSettings(component.getAzureSettingsForm()); + applyYouSettings(component.getYouSettingsForm()); + applyLlamaSettings(component.getLlamaSettingsForm()); var serviceChanged = component.getSelectedService() != settings.getSelectedService(); var modelChanged = !OpenAISettings.getCurrentState().getModel() @@ -109,7 +105,7 @@ private void applyOpenAISettings(OpenAISettingsForm form) { private void applyCustomOpenAISettings(CustomServiceForm form) { CredentialsStore.INSTANCE.setCredential(CUSTOM_SERVICE_API_KEY, form.getApiKey()); - CustomServiceSettings.getInstance().loadState(form.getCurrentState()); + form.applyChanges(); } private void applyLlamaSettings(LlamaSettingsForm form) { @@ -142,7 +138,7 @@ public void reset() { var settings = GeneralSettings.getCurrentState(); component.setDisplayName(settings.getDisplayName()); component.setSelectedService(settings.getSelectedService()); - component.getServiceSelectionForm().resetForms(); + component.resetForms(); } @Override diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceSelectionForm.java b/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceSelectionForm.java deleted file mode 100644 index 3aa63e758..000000000 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceSelectionForm.java +++ /dev/null @@ -1,68 +0,0 @@ -package ee.carlrobert.codegpt.settings.service; - -import com.intellij.openapi.Disposable; -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.custom.CustomServiceForm; -import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings; -import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; -import ee.carlrobert.codegpt.settings.service.llama.form.LlamaSettingsForm; -import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; -import ee.carlrobert.codegpt.settings.service.openai.OpenAISettingsForm; -import ee.carlrobert.codegpt.settings.service.you.YouSettings; -import ee.carlrobert.codegpt.settings.service.you.YouSettingsForm; - -public class ServiceSelectionForm { - - private final OpenAISettingsForm openAISettingsForm; - private final CustomServiceForm customServiceForm; - private final AnthropicSettingsForm anthropicSettingsForm; - private final AzureSettingsForm azureSettingsForm; - private final LlamaSettingsForm llamaSettingsForm; - private final YouSettingsForm youSettingsForm; - - public ServiceSelectionForm(Disposable parentDisposable) { - openAISettingsForm = new OpenAISettingsForm(OpenAISettings.getCurrentState()); - customServiceForm = new CustomServiceForm( - CustomServiceSettings.getCurrentState()); - anthropicSettingsForm = new AnthropicSettingsForm(AnthropicSettings.getCurrentState()); - azureSettingsForm = new AzureSettingsForm(AzureSettings.getCurrentState()); - youSettingsForm = new YouSettingsForm(YouSettings.getCurrentState(), parentDisposable); - llamaSettingsForm = new LlamaSettingsForm(LlamaSettings.getCurrentState()); - } - - public OpenAISettingsForm getOpenAISettingsForm() { - return openAISettingsForm; - } - - public CustomServiceForm getCustomConfigurationSettingsForm() { - return customServiceForm; - } - - public AnthropicSettingsForm getAnthropicSettingsForm() { - return anthropicSettingsForm; - } - - public AzureSettingsForm getAzureSettingsForm() { - return azureSettingsForm; - } - - public YouSettingsForm getYouSettingsForm() { - return youSettingsForm; - } - - public LlamaSettingsForm getLlamaSettingsForm() { - return llamaSettingsForm; - } - - public void resetForms() { - openAISettingsForm.resetForm(); - customServiceForm.resetForm(); - anthropicSettingsForm.resetForm(); - azureSettingsForm.resetForm(); - youSettingsForm.resetForm(); - llamaSettingsForm.resetForm(); - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceForm.java b/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceForm.java deleted file mode 100644 index f05be3cde..000000000 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceForm.java +++ /dev/null @@ -1,175 +0,0 @@ -package ee.carlrobert.codegpt.settings.service.custom; - -import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CUSTOM_SERVICE_API_KEY; -import static ee.carlrobert.codegpt.ui.UIUtil.withEmptyLeftBorder; - -import com.intellij.icons.AllIcons.General; -import com.intellij.ide.HelpTooltip; -import com.intellij.openapi.ui.ComboBox; -import com.intellij.openapi.ui.MessageType; -import com.intellij.ui.EnumComboBoxModel; -import com.intellij.ui.TitledSeparator; -import com.intellij.ui.components.JBLabel; -import com.intellij.ui.components.JBPasswordField; -import com.intellij.ui.components.JBTextField; -import com.intellij.util.ui.FormBuilder; -import ee.carlrobert.codegpt.CodeGPTBundle; -import ee.carlrobert.codegpt.completions.CallParameters; -import ee.carlrobert.codegpt.completions.CompletionRequestProvider; -import ee.carlrobert.codegpt.completions.CompletionRequestService; -import ee.carlrobert.codegpt.conversations.Conversation; -import ee.carlrobert.codegpt.conversations.message.Message; -import ee.carlrobert.codegpt.credentials.CredentialsStore; -import ee.carlrobert.codegpt.ui.OverlayUtil; -import ee.carlrobert.codegpt.ui.UIUtil; -import ee.carlrobert.llm.client.openai.completion.ErrorDetails; -import ee.carlrobert.llm.completion.CompletionEventListener; -import java.awt.BorderLayout; -import java.awt.FlowLayout; -import java.net.MalformedURLException; -import java.net.URL; -import javax.swing.Box; -import javax.swing.JButton; -import javax.swing.JPanel; -import javax.swing.SwingUtilities; -import okhttp3.sse.EventSource; -import org.jetbrains.annotations.Nullable; - -public class CustomServiceForm { - - private final JBPasswordField apiKeyField; - private final JBTextField urlField; - private final CustomServiceFormTabbedPane tabbedPane; - private final JButton testConnectionButton; - private final JBLabel templateHelpText; - private final ComboBox templateComboBox; - - public CustomServiceForm(CustomServiceSettingsState settings) { - apiKeyField = new JBPasswordField(); - apiKeyField.setColumns(30); - apiKeyField.setText(CredentialsStore.INSTANCE.getCredential(CUSTOM_SERVICE_API_KEY)); - urlField = new JBTextField(settings.getUrl(), 30); - tabbedPane = new CustomServiceFormTabbedPane(settings); - testConnectionButton = new JButton(CodeGPTBundle.get( - "settingsConfigurable.service.custom.openai.testConnection.label")); - testConnectionButton.addActionListener(e -> testConnection(getCurrentState())); - templateHelpText = new JBLabel(General.ContextHelp); - templateComboBox = new ComboBox<>( - new EnumComboBoxModel<>(CustomServiceTemplate.class)); - templateComboBox.setSelectedItem(settings.getTemplate()); - templateComboBox.addItemListener(e -> { - var template = (CustomServiceTemplate) e.getItem(); - updateTemplateHelpTextTooltip(template); - urlField.setText(template.getUrl()); - tabbedPane.setHeaders(template.getHeaders()); - tabbedPane.setBody(template.getBody()); - }); - updateTemplateHelpTextTooltip(settings.getTemplate()); - } - - public JPanel getForm() { - var urlPanel = new JPanel(new BorderLayout(8, 0)); - urlPanel.add(urlField, BorderLayout.CENTER); - urlPanel.add(testConnectionButton, BorderLayout.EAST); - - var templateComboBoxWrapper = new JPanel(new FlowLayout(FlowLayout.LEADING, 0, 0)); - templateComboBoxWrapper.add(templateComboBox); - templateComboBoxWrapper.add(Box.createHorizontalStrut(8)); - templateComboBoxWrapper.add(templateHelpText); - - var form = FormBuilder.createFormBuilder() - .addLabeledComponent( - CodeGPTBundle.get("settingsConfigurable.service.custom.openai.presetTemplate.label"), - templateComboBoxWrapper) - .addLabeledComponent( - CodeGPTBundle.get("settingsConfigurable.shared.apiKey.label"), - apiKeyField) - .addComponentToRightColumn( - UIUtil.createComment("settingsConfigurable.service.custom.openai.apiKey.comment")) - .addLabeledComponent( - CodeGPTBundle.get("settingsConfigurable.service.custom.openai.url.label"), - urlPanel) - .addComponent(tabbedPane) - .getPanel(); - - return FormBuilder.createFormBuilder() - .addComponent(new TitledSeparator(CodeGPTBundle.get("shared.configuration"))) - .addComponent(withEmptyLeftBorder(form)) - .addComponentFillVertically(new JPanel(), 0) - .getPanel(); - } - - public @Nullable String getApiKey() { - var apiKey = new String(apiKeyField.getPassword()); - return apiKey.isEmpty() ? null : apiKey; - } - - public CustomServiceSettingsState getCurrentState() { - var state = new CustomServiceSettingsState(); - state.setUrl(urlField.getText()); - state.setTemplate(templateComboBox.getItem()); - state.setHeaders(tabbedPane.getHeaders()); - state.setBody(tabbedPane.getBody()); - return state; - } - - public void resetForm() { - var state = CustomServiceSettings.getCurrentState(); - apiKeyField.setText(CredentialsStore.INSTANCE.getCredential(CUSTOM_SERVICE_API_KEY)); - urlField.setText(state.getUrl()); - templateComboBox.setSelectedItem(state.getTemplate()); - tabbedPane.setHeaders(state.getHeaders()); - tabbedPane.setBody(state.getBody()); - } - - private void updateTemplateHelpTextTooltip(CustomServiceTemplate template) { - templateHelpText.setToolTipText(null); - try { - new HelpTooltip() - .setTitle(template.getName()) - .setBrowserLink( - CodeGPTBundle.get("settingsConfigurable.service.custom.openai.linkToDocs"), - new URL(template.getDocsUrl())) - .installOn(templateHelpText); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - } - - private void testConnection(CustomServiceSettingsState customConfiguration) { - var conversation = new Conversation(); - var request = new CompletionRequestProvider(conversation) - .buildCustomOpenAIChatCompletionRequest( - customConfiguration, - new CallParameters(conversation, new Message("Hello!"))); - CompletionRequestService.getInstance() - .getCustomOpenAIChatCompletionAsync(request, new TestConnectionEventListener()); - } - - class TestConnectionEventListener implements CompletionEventListener { - - @Override - public void onMessage(String value, EventSource eventSource) { - if (value != null && !value.isEmpty()) { - SwingUtilities.invokeLater(() -> { - OverlayUtil.showBalloon( - CodeGPTBundle.get("settingsConfigurable.service.custom.openai.connectionSuccess"), - MessageType.INFO, - testConnectionButton); - eventSource.cancel(); - }); - } - } - - @Override - public void onError(ErrorDetails error, Throwable ex) { - SwingUtilities.invokeLater(() -> - OverlayUtil.showBalloon( - CodeGPTBundle.get("settingsConfigurable.service.custom.openai.connectionFailed") - + "\n\n" - + error.getMessage(), - MessageType.ERROR, - testConnectionButton)); - } - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceFormTabbedPane.java b/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceFormTabbedPane.java index b318bef79..2b5d828f3 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceFormTabbedPane.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceFormTabbedPane.java @@ -18,12 +18,13 @@ class CustomServiceFormTabbedPane extends JBTabbedPane { private final JBTable headersTable; private final JBTable bodyTable; - CustomServiceFormTabbedPane(CustomServiceSettingsState customConfiguration) { + CustomServiceFormTabbedPane(Map headers, Map body) { headersTable = new JBTable( - new DefaultTableModel(toArray(customConfiguration.getHeaders()), + new DefaultTableModel(toArray(headers), new Object[]{"Key", "Value"})); + bodyTable = new JBTable( - new DefaultTableModel(toArray(customConfiguration.getBody()), + new DefaultTableModel(toArray(body), new Object[]{"Key", "Value"})); setTabComponentInsets(JBUI.insetsTop(8)); @@ -46,11 +47,11 @@ public Map getHeaders() { .collect(toMap(Entry::getKey, entry -> (String) entry.getValue())); } - public void setBody(Map body) { + public void setBody(Map body) { setTableData(bodyTable, body); } - public Map getBody() { + public Map getBody() { return getTableData(bodyTable); } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.java b/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.java deleted file mode 100644 index f65fa3c51..000000000 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.java +++ /dev/null @@ -1,45 +0,0 @@ -package ee.carlrobert.codegpt.settings.service.custom; - -import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CUSTOM_SERVICE_API_KEY; - -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.credentials.CredentialsStore; -import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.NotNull; - -@State( - name = "CodeGPT_CustomServiceSettings", - storages = @Storage("CodeGPT_CustomServiceSettings.xml")) -public class CustomServiceSettings implements PersistentStateComponent { - - private CustomServiceSettingsState state = new CustomServiceSettingsState(); - - @Override - @NotNull - public CustomServiceSettingsState getState() { - return state; - } - - @Override - public void loadState(@NotNull CustomServiceSettingsState state) { - this.state = state; - } - - public static CustomServiceSettingsState getCurrentState() { - return getInstance().getState(); - } - - public static CustomServiceSettings getInstance() { - return ApplicationManager.getApplication().getService(CustomServiceSettings.class); - } - - public boolean isModified(CustomServiceForm form) { - return !form.getCurrentState().equals(state) - || !StringUtils.equals( - form.getApiKey(), - CredentialsStore.INSTANCE.getCredential(CUSTOM_SERVICE_API_KEY)); - } -} \ No newline at end of file diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettingsState.java b/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettingsState.java deleted file mode 100644 index cc93872ee..000000000 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettingsState.java +++ /dev/null @@ -1,69 +0,0 @@ -package ee.carlrobert.codegpt.settings.service.custom; - -import static ee.carlrobert.codegpt.settings.service.custom.CustomServiceTemplate.OPENAI; - -import com.intellij.util.xmlb.annotations.OptionTag; -import ee.carlrobert.codegpt.util.MapConverter; -import java.util.Map; -import java.util.Objects; - -public class CustomServiceSettingsState { - - private String url = OPENAI.getUrl(); - private Map headers = OPENAI.getHeaders(); - @OptionTag(converter = MapConverter.class) - private Map body = OPENAI.getBody(); - private CustomServiceTemplate template = OPENAI; - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public Map getHeaders() { - return headers; - } - - public void setHeaders(Map headers) { - this.headers = headers; - } - - public Map getBody() { - return body; - } - - public void setBody(Map body) { - this.body = body; - } - - public CustomServiceTemplate getTemplate() { - return template; - } - - public void setTemplate(CustomServiceTemplate template) { - this.template = template; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - CustomServiceSettingsState that = (CustomServiceSettingsState) o; - return Objects.equals(url, that.url) - && Objects.equals(headers, that.headers) - && Objects.equals(body, that.body) - && template == that.template; - } - - @Override - public int hashCode() { - return Objects.hash(url, headers, body, template); - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceTemplate.java b/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceTemplate.java deleted file mode 100644 index 49226a4cd..000000000 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceTemplate.java +++ /dev/null @@ -1,158 +0,0 @@ -package ee.carlrobert.codegpt.settings.service.custom; - -import java.util.HashMap; -import java.util.Map; - -public enum CustomServiceTemplate { - - // Cloud providers - ANYSCALE( - "Anyscale", - "https://docs.endpoints.anyscale.com/", - "https://api.endpoints.anyscale.com/v1/chat/completions", - getDefaultHeadersWithAuthentication(), - getDefaultBodyParams(Map.of( - "model", "mistralai/Mixtral-8x7B-Instruct-v0.1", - "max_tokens", 1024))), - AZURE( - "Azure OpenAI", - "https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions", - "https://{your-resource-name}.openai.azure.com/openai/deployments/{deployment-id}/chat/completions?api-version=2023-05-15", - getDefaultHeaders("api-key", "$CUSTOM_SERVICE_API_KEY"), - getDefaultBodyParams(Map.of())), - DEEP_INFRA( - "DeepInfra", - "https://deepinfra.com/docs/advanced/openai_api", - "https://api.deepinfra.com/v1/openai/chat/completions", - getDefaultHeadersWithAuthentication(), - getDefaultBodyParams(Map.of( - "model", "meta-llama/Llama-2-70b-chat-hf", - "max_tokens", 1024))), - FIREWORKS( - "Fireworks", - "https://readme.fireworks.ai/reference/createchatcompletion", - "https://api.fireworks.ai/inference/v1/chat/completions", - getDefaultHeadersWithAuthentication(), - getDefaultBodyParams(Map.of( - "model", "accounts/fireworks/models/llama-v2-7b-chat", - "max_tokens", 1024))), - GROQ( - "Groq", - "https://docs.api.groq.com/md/openai.oas.html", - "https://api.groq.com/openai/v1/chat/completions", - getDefaultHeadersWithAuthentication(), - getDefaultBodyParams(Map.of( - "model", "codellama-34b", - "max_tokens", 1024))), - OPENAI( - "OpenAI", - "https://platform.openai.com/docs/api-reference/chat", - "https://api.openai.com/v1/chat/completions", - getDefaultHeaders("Authorization", "Bearer $CUSTOM_SERVICE_API_KEY"), - getDefaultBodyParams(Map.of( - "model", "gpt-4", - "max_tokens", 1024))), - PERPLEXITY( - "Perplexity AI", - "https://docs.perplexity.ai/reference/post_chat_completions", - "https://api.perplexity.ai/chat/completions", - getDefaultHeadersWithAuthentication(), - getDefaultBodyParams(Map.of( - "model", "codellama", - "max_tokens", 1024))), - TOGETHER( - "Together AI", - "https://docs.together.ai/docs/openai-api-compatibility", - "https://api.together.xyz/v1/chat/completions", - getDefaultHeaders("Authorization", "Bearer $CUSTOM_SERVICE_API_KEY"), - getDefaultBodyParams(Map.of( - "model", "deepseek-ai/deepseek-coder-33b-instruct", - "max_tokens", 1024))), - - // Local providers - OLLAMA( - "Ollama", - "https://github.com/ollama/ollama/blob/main/docs/openai.md", - "http://localhost:11434/v1/chat/completions", - getDefaultHeaders(), - getDefaultBodyParams(Map.of("model", "codellama"))), - LLAMA_CPP( - "LLaMA C/C++", - "https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md", - "http://localhost:8080/v1/chat/completions", - getDefaultHeaders(), - getDefaultBodyParams(Map.of())); - - private final String name; - private final String docsUrl; - private final String url; - private final Map headers; - private final Map body; - - CustomServiceTemplate( - String name, - String docsUrl, - String url, - Map headers, - Map body) { - this.name = name; - this.docsUrl = docsUrl; - this.url = url; - this.headers = headers; - this.body = body; - } - - public String getName() { - return name; - } - - public String getDocsUrl() { - return docsUrl; - } - - public String getUrl() { - return url; - } - - public Map getHeaders() { - return headers; - } - - public Map getBody() { - return body; - } - - @Override - public String toString() { - return name; - } - - private static Map getDefaultHeadersWithAuthentication() { - return getDefaultHeaders("Authorization", "Bearer $CUSTOM_SERVICE_API_KEY"); - } - - private static Map getDefaultHeaders() { - return getDefaultHeaders(Map.of()); - } - - private static Map getDefaultHeaders(String key, String value) { - return getDefaultHeaders(Map.of(key, value)); - } - - private static Map getDefaultHeaders(Map additionalHeaders) { - var defaultHeaders = new HashMap<>(Map.of( - "Content-Type", "application/json", - "X-LLM-Application-Tag", "codegpt")); - defaultHeaders.putAll(additionalHeaders); - return defaultHeaders; - } - - private static Map getDefaultBodyParams(Map additionalParams) { - var defaultParams = new HashMap(Map.of( - "stream", true, - "messages", "$OPENAI_MESSAGES", - "temperature", 0.1)); - defaultParams.putAll(additionalParams); - return defaultParams; - } -} 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 1d1b14b53..71aef2448 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 @@ -82,7 +82,10 @@ public JComponent createCustomComponent( actionGroup.addSeparator("Custom OpenAI Service"); actionGroup.add(createModelAction( CUSTOM_OPENAI, - CustomServiceSettings.getCurrentState().getTemplate().getName(), + ApplicationManager.getApplication().getService(CustomServiceSettings.class) + .getState() + .getTemplate() + .getProviderName(), Icons.OpenAI, presentation)); actionGroup.addSeparator(); @@ -150,9 +153,11 @@ private void updateTemplatePresentation(ServiceType selectedService) { break; case CUSTOM_OPENAI: templatePresentation.setIcon(Icons.OpenAI); - templatePresentation.setText(CustomServiceSettings.getCurrentState() - .getTemplate() - .getName()); + templatePresentation.setText( + ApplicationManager.getApplication().getService(CustomServiceSettings.class) + .getState() + .getTemplate() + .getProviderName()); break; case ANTHROPIC: templatePresentation.setIcon(Icons.Anthropic); diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt index c9dc223e7..bf5d71455 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt @@ -2,11 +2,12 @@ package ee.carlrobert.codegpt.actions 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.settings.GeneralSettings import ee.carlrobert.codegpt.settings.service.ServiceType -import ee.carlrobert.codegpt.settings.service.ServiceType.LLAMA_CPP -import ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI +import ee.carlrobert.codegpt.settings.service.ServiceType.* +import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings @@ -14,12 +15,16 @@ abstract class CodeCompletionFeatureToggleActions( private val enableFeatureAction: Boolean ) : DumbAwareAction() { + override fun actionPerformed(e: AnActionEvent) { GeneralSettings.getCurrentState().selectedService - .takeIf { it in listOf(OPENAI, LLAMA_CPP) } + .takeIf { it in listOf(OPENAI, CUSTOM_OPENAI, LLAMA_CPP) } ?.also { selectedService -> if (OPENAI == selectedService) { OpenAISettings.getCurrentState().isCodeCompletionsEnabled = enableFeatureAction + } else if (CUSTOM_OPENAI == selectedService) { + service().state.codeCompletionSettings.codeCompletionsEnabled = + enableFeatureAction } else { LlamaSettings.getCurrentState().isCodeCompletionsEnabled = enableFeatureAction } @@ -31,7 +36,7 @@ abstract class CodeCompletionFeatureToggleActions( val codeCompletionEnabled = isCodeCompletionsEnabled(selectedService) e.presentation.isEnabled = codeCompletionEnabled != enableFeatureAction e.presentation.isVisible = - e.presentation.isEnabled && listOf(OPENAI, LLAMA_CPP).contains( + e.presentation.isEnabled && listOf(OPENAI, CUSTOM_OPENAI, LLAMA_CPP).contains( selectedService ) } @@ -43,6 +48,7 @@ abstract class CodeCompletionFeatureToggleActions( private fun isCodeCompletionsEnabled(serviceType: ServiceType): Boolean { return when (serviceType) { OPENAI -> OpenAISettings.getCurrentState().isCodeCompletionsEnabled + CUSTOM_OPENAI -> service().state.codeCompletionSettings.codeCompletionsEnabled LLAMA_CPP -> LlamaSettings.getCurrentState().isCodeCompletionsEnabled else -> false } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt index 26c35063d..d7723f0d2 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt @@ -1,13 +1,26 @@ package ee.carlrobert.codegpt.codecompletions +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.ObjectMapper +import com.intellij.openapi.components.service 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.custom.CustomServiceSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettingsState import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings import ee.carlrobert.llm.client.llama.completion.LlamaCompletionRequest import ee.carlrobert.llm.client.openai.completion.request.OpenAITextCompletionRequest +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import java.nio.charset.StandardCharsets object CodeCompletionRequestFactory { + + @JvmStatic fun buildOpenAIRequest(details: InfillRequestDetails): OpenAITextCompletionRequest { return OpenAITextCompletionRequest.Builder(details.prefix) .setSuffix(details.suffix) @@ -17,6 +30,35 @@ object CodeCompletionRequestFactory { .build() } + @JvmStatic + fun buildCustomRequest(details: InfillRequestDetails): Request { + val settings = service().state.codeCompletionSettings + val requestBuilder = Request.Builder().url(settings.url!!) + val credential = getCredential(CredentialKey.CUSTOM_SERVICE_API_KEY) + for (entry in settings.headers.entries) { + var value = entry.value + if (credential != null && value.contains("\$CUSTOM_SERVICE_API_KEY")) { + value = value.replace("\$CUSTOM_SERVICE_API_KEY", credential) + } + requestBuilder.addHeader(entry.key, value) + } + val transformedBody = settings.body.entries.associate { (key, value) -> + key to transformValue(value, settings.infillTemplate, details) + } + + try { + val requestBody = ObjectMapper() + .writerWithDefaultPrettyPrinter() + .writeValueAsString(transformedBody) + .toByteArray(StandardCharsets.UTF_8) + .toRequestBody("application/json".toMediaType()) + return requestBuilder.post(requestBody).build() + } catch (e: JsonProcessingException) { + throw RuntimeException(e) + } + } + + @JvmStatic fun buildLlamaRequest(details: InfillRequestDetails): LlamaCompletionRequest { val settings = LlamaSettings.getCurrentState() val promptTemplate = getLlamaInfillPromptTemplate(settings) @@ -38,4 +80,18 @@ object CodeCompletionRequestFactory { } return LlamaModel.findByHuggingFaceModel(settings.huggingFaceModel).infillPromptTemplate } + + private fun transformValue( + value: Any, + template: InfillPromptTemplate, + details: InfillRequestDetails + ): Any { + if (value !is String) return value + return when (value) { + "$" + Placeholder.FIM_PROMPT -> template.buildPrompt(details.prefix, details.suffix) + "$" + Placeholder.PREFIX -> details.prefix + "$" + Placeholder.SUFFIX -> details.suffix + else -> value + } + } } \ 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 91f59474e..9812902f0 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt @@ -2,14 +2,19 @@ package ee.carlrobert.codegpt.codecompletions import com.intellij.codeInsight.inline.completion.* import com.intellij.codeInsight.inline.completion.elements.InlineCompletionGrayTextElement +import com.intellij.notification.NotificationType import com.intellij.openapi.application.EDT -import com.intellij.openapi.diagnostic.Logger +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.custom.CustomServiceSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings +import ee.carlrobert.codegpt.ui.OverlayUtil.showNotification +import ee.carlrobert.llm.client.openai.completion.ErrorDetails import ee.carlrobert.llm.completion.CompletionEventListener import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose @@ -20,9 +25,8 @@ import okhttp3.sse.EventSource import java.util.concurrent.atomic.AtomicReference class CodeGPTInlineCompletionProvider : InlineCompletionProvider { - companion object { - private val LOG = Logger.getInstance(CodeGPTInlineCompletionProvider::class.java) + private val logger = thisLogger() } private val currentCall = AtomicReference(null) @@ -32,7 +36,7 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { override suspend fun getSuggestion(request: InlineCompletionRequest): InlineCompletionSuggestion { if (request.editor.project == null) { - LOG.error("Could not find project") + logger.error("Could not find project") return InlineCompletionSuggestion.empty() } @@ -50,7 +54,7 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { try { trySend(InlineCompletionGrayTextElement(inlineText)) } catch (e: Exception) { - LOG.error("Failed to send inline completion suggestion", e) + logger.error("Failed to send inline completion suggestion", e) } } } @@ -64,6 +68,7 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { val selectedService = GeneralSettings.getCurrentState().selectedService val codeCompletionsEnabled = when (selectedService) { ServiceType.OPENAI -> OpenAISettings.getCurrentState().isCodeCompletionsEnabled + ServiceType.CUSTOM_OPENAI -> service().state.codeCompletionSettings.codeCompletionsEnabled ServiceType.LLAMA_CPP -> LlamaSettings.getCurrentState().isCodeCompletionsEnabled else -> false } @@ -91,5 +96,12 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { override fun onCancelled(messageBuilder: StringBuilder) { completed(messageBuilder) } + + override fun onError(error: ErrorDetails, ex: Throwable) { + if (ex.message == null || (ex.message != null && ex.message != "Canceled")) { + showNotification(error.message, NotificationType.ERROR) + logger.error(error.message, ex) + } + } } } \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/InfillRequestDetails.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/InfillRequestDetails.kt index 3b207f90c..7dd466643 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/InfillRequestDetails.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/InfillRequestDetails.kt @@ -4,11 +4,10 @@ import com.intellij.codeInsight.inline.completion.InlineCompletionRequest import com.intellij.openapi.editor.Document import com.intellij.openapi.util.TextRange import ee.carlrobert.codegpt.EncodingManager -import ee.carlrobert.codegpt.util.file.FileUtil import kotlin.math.max import kotlin.math.min -class InfillRequestDetails(val prefix: String, val suffix: String, val fileExtension: String) { +class InfillRequestDetails(val prefix: String, val suffix: String) { companion object { private const val MAX_OFFSET = 10_000 private const val MAX_PROMPT_TOKENS = 128 @@ -17,18 +16,16 @@ class InfillRequestDetails(val prefix: String, val suffix: String, val fileExten return fromDocumentWithMaxOffset( request.editor.document, request.editor.caretModel.offset, - FileUtil.getFileExtension(request.file.name) ) } private fun fromDocumentWithMaxOffset( document: Document, caretOffset: Int, - fileExtension: String ): InfillRequestDetails { val start = max(0, (caretOffset - MAX_OFFSET)) val end = min(document.textLength, (caretOffset + MAX_OFFSET)) - return fromDocumentWithCustomRange(document, caretOffset, start, end, fileExtension) + return fromDocumentWithCustomRange(document, caretOffset, start, end) } private fun fromDocumentWithCustomRange( @@ -36,11 +33,10 @@ class InfillRequestDetails(val prefix: String, val suffix: String, val fileExten caretOffset: Int, start: Int, end: Int, - fileExtension: String ): InfillRequestDetails { val prefix: String = truncateText(document, start, caretOffset, false) val suffix: String = truncateText(document, caretOffset, end, true) - return InfillRequestDetails(prefix, suffix, fileExtension) + return InfillRequestDetails(prefix, suffix) } private fun truncateText( diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/Placeholder.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/Placeholder.kt index 9a940907a..20f8ce15a 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/Placeholder.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/Placeholder.kt @@ -7,7 +7,10 @@ import java.time.LocalDate enum class Placeholder(val description: String) { DATE_ISO_8601("Current date in ISO 8601 format, e.g. 2021-01-01."), - BRANCH_NAME("The name of the current branch") + BRANCH_NAME("The name of the current branch."), + PREFIX("Code before the cursor."), + SUFFIX("Code after the cursor."), + FIM_PROMPT("Prebuilt Fill-In-The-Middle (FIM) prompt using the specified template."), } interface PlaceholderStrategy { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceChatCompletionForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceChatCompletionForm.kt new file mode 100644 index 000000000..7bf46f95d --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceChatCompletionForm.kt @@ -0,0 +1,101 @@ +package ee.carlrobert.codegpt.settings.service.custom + +import com.intellij.openapi.ui.MessageType +import com.intellij.ui.components.JBTextField +import com.intellij.util.ui.FormBuilder +import ee.carlrobert.codegpt.CodeGPTBundle +import ee.carlrobert.codegpt.completions.CompletionRequestProvider +import ee.carlrobert.codegpt.completions.CompletionRequestService +import ee.carlrobert.codegpt.ui.OverlayUtil +import ee.carlrobert.llm.client.openai.completion.ErrorDetails +import ee.carlrobert.llm.completion.CompletionEventListener +import okhttp3.sse.EventSource +import java.awt.BorderLayout +import javax.swing.JButton +import javax.swing.JPanel +import javax.swing.SwingUtilities + +class CustomServiceChatCompletionForm(state: CustomServiceChatCompletionSettingsState) { + + private val urlField = JBTextField(state.url, 30) + private val tabbedPane = CustomServiceFormTabbedPane(state.headers, state.body) + private val testConnectionButton = JButton( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.testConnection.label") + ) + + init { + testConnectionButton.addActionListener { testConnection() } + } + + var url: String + get() = urlField.text + set(url) { + urlField.text = url + } + + var headers: MutableMap + get() = tabbedPane.headers + set(value) { + tabbedPane.headers = value + } + + var body: MutableMap + get() = tabbedPane.body + set(value) { + tabbedPane.body = value + } + + val form: JPanel + get() = FormBuilder.createFormBuilder() + .addVerticalGap(8) + .addLabeledComponent( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.url.label"), + JPanel(BorderLayout(8, 0)).apply { + add(urlField, BorderLayout.CENTER) + add(testConnectionButton, BorderLayout.EAST) + } + ) + .addComponent(tabbedPane) + .addComponentFillVertically(JPanel(), 0) + .panel + + fun resetForm(settings: CustomServiceChatCompletionSettingsState) { + urlField.text = settings.url + tabbedPane.headers = settings.headers + tabbedPane.body = settings.body + } + + private fun testConnection() { + CompletionRequestService.getInstance().getCustomOpenAIChatCompletionAsync( + CompletionRequestProvider.buildCustomOpenAICompletionRequest("Hello!"), + TestConnectionEventListener() + ) + } + + internal inner class TestConnectionEventListener : CompletionEventListener { + override fun onMessage(value: String?, eventSource: EventSource) { + if (!value.isNullOrEmpty()) { + SwingUtilities.invokeLater { + OverlayUtil.showBalloon( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.connectionSuccess"), + MessageType.INFO, + testConnectionButton + ) + eventSource.cancel() + } + } + } + + override fun onError(error: ErrorDetails, ex: Throwable) { + SwingUtilities.invokeLater { + OverlayUtil.showBalloon( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.connectionFailed") + + "\n\n" + + error.message, + MessageType.ERROR, + testConnectionButton + ) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceChatCompletionTemplate.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceChatCompletionTemplate.kt new file mode 100644 index 000000000..f6d392717 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceChatCompletionTemplate.kt @@ -0,0 +1,124 @@ +package ee.carlrobert.codegpt.settings.service.custom + +enum class CustomServiceChatCompletionTemplate( + val url: String, + val headers: MutableMap, + val body: MutableMap +) { + ANYSCALE( + "https://api.endpoints.anyscale.com/v1/chat/completions", + getDefaultHeadersWithAuthentication(), + getDefaultBodyParams( + mapOf( + "model" to "mistralai/Mixtral-8x7B-Instruct-v0.1", + "max_tokens" to 1024 + ) + ) + ), + AZURE( + "https://{your-resource-name}.openai.azure.com/openai/deployments/{deployment-id}/chat/completions?api-version=2023-05-15", + getDefaultHeaders("api-key", "\$CUSTOM_SERVICE_API_KEY"), + getDefaultBodyParams(emptyMap()) + ), + DEEP_INFRA( + "https://api.deepinfra.com/v1/openai/chat/completions", + getDefaultHeadersWithAuthentication(), + getDefaultBodyParams( + mapOf( + "model" to "meta-llama/Llama-2-70b-chat-hf", + "max_tokens" to 1024 + ) + ) + ), + FIREWORKS( + "https://api.fireworks.ai/inference/v1/chat/completions", + getDefaultHeadersWithAuthentication(), + getDefaultBodyParams( + mapOf( + "model" to "accounts/fireworks/models/llama-v2-7b-chat", + "max_tokens" to 1024 + ) + ) + ), + GROQ( + "https://api.groq.com/openai/v1/chat/completions", + getDefaultHeadersWithAuthentication(), + getDefaultBodyParams( + mapOf( + "model" to "codellama-34b", + "max_tokens" to 1024 + ) + ) + ), + OPENAI( + "https://api.openai.com/v1/chat/completions", + getDefaultHeaders("Authorization", "Bearer \$CUSTOM_SERVICE_API_KEY"), + getDefaultBodyParams( + mapOf( + "model" to "gpt-4", + "max_tokens" to 1024 + ) + ) + ), + PERPLEXITY( + "https://api.perplexity.ai/chat/completions", + getDefaultHeadersWithAuthentication(), + getDefaultBodyParams( + mapOf( + "model" to "codellama", + "max_tokens" to 1024 + ) + ) + ), + TOGETHER( + "https://api.together.xyz/v1/chat/completions", + getDefaultHeaders("Authorization", "Bearer \$CUSTOM_SERVICE_API_KEY"), + getDefaultBodyParams( + mapOf( + "model" to "deepseek-ai/deepseek-coder-33b-instruct", + "max_tokens" to 1024 + ) + ) + ), + OLLAMA( + "http://localhost:11434/v1/chat/completions", + getDefaultHeaders(), + getDefaultBodyParams(mapOf("model" to "codellama")) + ), + LLAMA_CPP( + "http://localhost:8080/v1/chat/completions", + getDefaultHeaders(), + getDefaultBodyParams(emptyMap()) + ); +} + +private fun getDefaultHeadersWithAuthentication(): MutableMap { + return getDefaultHeaders("Authorization", "Bearer \$CUSTOM_SERVICE_API_KEY") +} + +private fun getDefaultHeaders(): MutableMap { + return getDefaultHeaders(emptyMap()) +} + +private fun getDefaultHeaders(key: String, value: String): MutableMap { + return getDefaultHeaders(mapOf(key to value)) +} + +private fun getDefaultHeaders(additionalHeaders: Map): MutableMap { + val defaultHeaders = mutableMapOf( + "Content-Type" to "application/json", + "X-LLM-Application-Tag" to "codegpt" + ) + defaultHeaders.putAll(additionalHeaders) + return defaultHeaders +} + +private fun getDefaultBodyParams(additionalParams: Map): MutableMap { + val defaultParams = mutableMapOf( + "stream" to true, + "messages" to "\$OPENAI_MESSAGES", + "temperature" to 0.1 + ) + defaultParams.putAll(additionalParams) + return defaultParams +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceCodeCompletionForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceCodeCompletionForm.kt new file mode 100644 index 000000000..fcac639b9 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceCodeCompletionForm.kt @@ -0,0 +1,182 @@ +package ee.carlrobert.codegpt.settings.service.custom + +import com.intellij.icons.AllIcons.General +import com.intellij.ide.HelpTooltip +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.MessageType +import com.intellij.openapi.ui.panel.ComponentPanelBuilder +import com.intellij.ui.EnumComboBoxModel +import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBTextField +import com.intellij.util.ui.FormBuilder +import ee.carlrobert.codegpt.CodeGPTBundle +import ee.carlrobert.codegpt.codecompletions.CodeCompletionRequestFactory +import ee.carlrobert.codegpt.codecompletions.InfillPromptTemplate +import ee.carlrobert.codegpt.codecompletions.InfillRequestDetails +import ee.carlrobert.codegpt.completions.CompletionRequestService +import ee.carlrobert.codegpt.settings.configuration.Placeholder +import ee.carlrobert.codegpt.ui.OverlayUtil +import ee.carlrobert.llm.client.openai.completion.ErrorDetails +import ee.carlrobert.llm.completion.CompletionEventListener +import okhttp3.sse.EventSource +import org.apache.commons.text.StringEscapeUtils +import java.awt.BorderLayout +import java.awt.FlowLayout +import javax.swing.Box +import javax.swing.JButton +import javax.swing.JPanel +import javax.swing.SwingUtilities + +class CustomServiceCodeCompletionForm(state: CustomServiceCodeCompletionSettingsState) { + + private val featureEnabledCheckBox = JBCheckBox( + CodeGPTBundle.get("codeCompletionsForm.enableFeatureText"), + state.codeCompletionsEnabled + ) + private val promptTemplateComboBox = + ComboBox(EnumComboBoxModel(InfillPromptTemplate::class.java)).apply { + selectedItem = state.infillTemplate + setSelectedItem(InfillPromptTemplate.LLAMA) + addItemListener { + updatePromptTemplateHelpTooltip(it.item as InfillPromptTemplate) + } + } + private val promptTemplateHelpText = JBLabel(General.ContextHelp) + private val urlField = JBTextField(state.url, 30) + private val tabbedPane = CustomServiceFormTabbedPane(state.headers, state.body) + private val testConnectionButton = JButton( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.testConnection.label") + ) + + init { + testConnectionButton.addActionListener { testConnection() } + updatePromptTemplateHelpTooltip(state.infillTemplate) + } + + var codeCompletionsEnabled: Boolean + get() = featureEnabledCheckBox.isSelected + set(enabled) { + featureEnabledCheckBox.isSelected = enabled + } + + var infillTemplate: InfillPromptTemplate + get() = promptTemplateComboBox.item + set(template) { + promptTemplateComboBox.selectedItem = template + } + + var url: String + get() = urlField.text + set(url) { + urlField.text = url + } + + var headers: MutableMap + get() = tabbedPane.headers + set(value) { + tabbedPane.headers = value + } + + var body: MutableMap + get() = tabbedPane.body + set(value) { + tabbedPane.body = value + } + + val form: JPanel + get() = FormBuilder.createFormBuilder() + .addVerticalGap(8) + .addComponent(featureEnabledCheckBox) + .addVerticalGap(4) + .addLabeledComponent( + "FIM template:", + JPanel(FlowLayout(FlowLayout.LEADING, 0, 0)).apply { + add(promptTemplateComboBox) + add(Box.createHorizontalStrut(4)) + add(promptTemplateHelpText) + }) + .addLabeledComponent( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.url.label"), + JPanel(BorderLayout(8, 0)).apply { + add(urlField, BorderLayout.CENTER) + add(testConnectionButton, BorderLayout.EAST) + } + ) + .addComponent(tabbedPane) + .addComponent(ComponentPanelBuilder.createCommentComponent(getHtmlDescription(), true, 100)) + .addComponentFillVertically(JPanel(), 0) + .panel + + private fun getHtmlDescription(): String { + val placeholderDescriptions = listOf( + Placeholder.FIM_PROMPT, + Placeholder.PREFIX, + Placeholder.SUFFIX + ).joinToString("\n") { + "
  • \$${it.name}: ${it.description}
  • " + } + + return buildString { + append("\n") + append("\n") + append("

    Use the following placeholders to insert dynamic values:

    \n") + append("
      $placeholderDescriptions
    \n") + append("\n") + append("") + } + } + + fun resetForm(settings: CustomServiceCodeCompletionSettingsState) { + featureEnabledCheckBox.isSelected = settings.codeCompletionsEnabled + promptTemplateComboBox.selectedItem = settings.infillTemplate + urlField.text = settings.url + tabbedPane.headers = settings.headers + tabbedPane.body = settings.body + updatePromptTemplateHelpTooltip(settings.infillTemplate) + } + + private fun testConnection() { + CompletionRequestService.getInstance().getCustomOpenAICompletionAsync( + CodeCompletionRequestFactory.buildCustomRequest(InfillRequestDetails("Hello", "!")), + TestConnectionEventListener() + ) + } + + internal inner class TestConnectionEventListener : CompletionEventListener { + override fun onMessage(value: String?, eventSource: EventSource) { + if (!value.isNullOrEmpty()) { + SwingUtilities.invokeLater { + OverlayUtil.showBalloon( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.connectionSuccess"), + MessageType.INFO, + testConnectionButton + ) + eventSource.cancel() + } + } + } + + override fun onError(error: ErrorDetails, ex: Throwable) { + SwingUtilities.invokeLater { + OverlayUtil.showBalloon( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.connectionFailed") + + "\n\n" + + error.message, + MessageType.ERROR, + testConnectionButton + ) + } + } + } + + private fun updatePromptTemplateHelpTooltip(template: InfillPromptTemplate) { + promptTemplateHelpText.setToolTipText(null) + + val description = StringEscapeUtils.escapeHtml4(template.buildPrompt("PREFIX", "SUFFIX")) + HelpTooltip() + .setTitle(template.toString()) + .setDescription("

    $description

    ") + .installOn(promptTemplateHelpText) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceCodeCompletionTemplate.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceCodeCompletionTemplate.kt new file mode 100644 index 000000000..98ca97ab8 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceCodeCompletionTemplate.kt @@ -0,0 +1,73 @@ +package ee.carlrobert.codegpt.settings.service.custom + +enum class CustomServiceCodeCompletionTemplate( + val url: String, + val headers: MutableMap, + val body: MutableMap +) { + ANYSCALE( + "https://api.endpoints.anyscale.com/v1/completions", + getDefaultHeadersWithAuthentication(), + getDefaultBodyParams(mapOf("model" to "codellama/CodeLlama-70b-Instruct-hf")) + ), + AZURE( + "https://{your-resource-name}.openai.azure.com/openai/deployments/{deployment-id}/completions?api-version=2023-05-15", + getDefaultHeaders("api-key", "\$CUSTOM_SERVICE_API_KEY"), + getDefaultBodyParams(emptyMap()) + ), + DEEP_INFRA( + "https://api.deepinfra.com/v1/inference/codellama/CodeLlama-70b-Instruct-hf", + getDefaultHeadersWithAuthentication(), + mutableMapOf("input" to "\$FIM_PROMPT") + ), + FIREWORKS( + "https://api.fireworks.ai/inference/v1/completions", + getDefaultHeadersWithAuthentication(), + getDefaultBodyParams(mapOf("model" to "accounts/fireworks/models/starcoder-16b")) + ), + OPENAI( + "https://api.openai.com/v1/completions", + getDefaultHeaders("Authorization", "Bearer \$CUSTOM_SERVICE_API_KEY"), + mutableMapOf( + "stream" to true, + "prompt" to "\$PREFIX", + "suffix" to "\$SUFFIX", + "model" to "gpt-3.5-turbo-instruct", + "temperature" to 0.2, + "max_tokens" to 24 + ) + ), + TOGETHER( + "https://api.together.xyz/v1/completions", + getDefaultHeaders("Authorization", "Bearer \$CUSTOM_SERVICE_API_KEY"), + getDefaultBodyParams(mapOf("model" to "codellama/CodeLlama-70b-hf")) + ) +} + +private fun getDefaultHeadersWithAuthentication(): MutableMap { + return getDefaultHeaders("Authorization", "Bearer \$CUSTOM_SERVICE_API_KEY") +} + +private fun getDefaultHeaders(key: String, value: String): MutableMap { + return getDefaultHeaders(mapOf(key to value)) +} + +private fun getDefaultHeaders(additionalHeaders: Map): MutableMap { + val defaultHeaders = mutableMapOf( + "Content-Type" to "application/json", + "X-LLM-Application-Tag" to "codegpt" + ) + defaultHeaders.putAll(additionalHeaders) + return defaultHeaders +} + +private fun getDefaultBodyParams(additionalParams: Map): MutableMap { + val defaultParams = mutableMapOf( + "stream" to true, + "prompt" to "\$FIM_PROMPT", + "temperature" to 0.2, + "max_tokens" to 24 + ) + defaultParams.putAll(additionalParams) + return defaultParams +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceForm.kt new file mode 100644 index 000000000..b5c8344dd --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceForm.kt @@ -0,0 +1,148 @@ +package ee.carlrobert.codegpt.settings.service.custom + +import com.intellij.icons.AllIcons.General +import com.intellij.ide.HelpTooltip +import com.intellij.openapi.components.service +import com.intellij.openapi.ui.ComboBox +import com.intellij.ui.EnumComboBoxModel +import com.intellij.ui.TitledSeparator +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBPasswordField +import com.intellij.util.ui.FormBuilder +import ee.carlrobert.codegpt.CodeGPTBundle +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey +import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential +import ee.carlrobert.codegpt.ui.UIUtil +import java.awt.FlowLayout +import java.net.MalformedURLException +import java.net.URL +import javax.swing.Box +import javax.swing.JPanel +import javax.swing.JTabbedPane + +class CustomServiceForm { + + private val apiKeyField = JBPasswordField().apply { + columns = 30 + text = getCredential(CredentialKey.CUSTOM_SERVICE_API_KEY) + } + private val templateHelpText = JBLabel(General.ContextHelp) + private val templateComboBox = ComboBox(EnumComboBoxModel(CustomServiceTemplate::class.java)) + private val chatCompletionsForm: CustomServiceChatCompletionForm + private val codeCompletionsForm: CustomServiceCodeCompletionForm + private val tabbedPane: JTabbedPane + + init { + val state = service().state + chatCompletionsForm = CustomServiceChatCompletionForm(state.chatCompletionSettings) + codeCompletionsForm = CustomServiceCodeCompletionForm(state.codeCompletionSettings) + tabbedPane = JTabbedPane().apply { + add(CodeGPTBundle.get("shared.chatCompletions"), chatCompletionsForm.form) + add(CodeGPTBundle.get("shared.codeCompletions"), codeCompletionsForm.form) + } + templateComboBox.selectedItem = state.template + templateComboBox.addItemListener { + val template = it.item as CustomServiceTemplate + updateTemplateHelpTextTooltip(template) + chatCompletionsForm.run { + url = template.chatCompletionTemplate.url + headers = template.chatCompletionTemplate.headers + body = template.chatCompletionTemplate.body + } + if (template.codeCompletionTemplate != null) { + codeCompletionsForm.run { + url = template.codeCompletionTemplate.url + headers = template.codeCompletionTemplate.headers + body = template.codeCompletionTemplate.body + } + tabbedPane.setEnabledAt(1, true) + } else { + tabbedPane.selectedIndex = 0 + tabbedPane.setEnabledAt(1, false) + } + } + updateTemplateHelpTextTooltip(state.template) + } + + fun getForm(): JPanel = FormBuilder.createFormBuilder() + .addComponent(TitledSeparator(CodeGPTBundle.get("shared.configuration"))) + .addComponent( + FormBuilder.createFormBuilder() + .setFormLeftIndent(16) + .addLabeledComponent( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.presetTemplate.label"), + JPanel(FlowLayout(FlowLayout.LEADING, 0, 0)).apply { + add(templateComboBox) + add(Box.createHorizontalStrut(8)) + add(templateHelpText) + } + ) + .addLabeledComponent( + CodeGPTBundle.get("settingsConfigurable.shared.apiKey.label"), + apiKeyField + ) + .addComponentToRightColumn( + UIUtil.createComment("settingsConfigurable.service.custom.openai.apiKey.comment") + ) + .addVerticalGap(4) + .addComponent(tabbedPane) + .panel + ) + .panel + + fun getApiKey() = String(apiKeyField.password).ifEmpty { null } + + fun isModified() = service().state.run { + templateComboBox.selectedItem != template + || chatCompletionsForm.url != chatCompletionSettings.url + || chatCompletionsForm.headers != chatCompletionSettings.headers + || chatCompletionsForm.body != chatCompletionSettings.body + || codeCompletionsForm.codeCompletionsEnabled != codeCompletionSettings.codeCompletionsEnabled + || codeCompletionsForm.infillTemplate != codeCompletionSettings.infillTemplate + || codeCompletionsForm.url != codeCompletionSettings.url + || codeCompletionsForm.headers != codeCompletionSettings.headers + || codeCompletionsForm.body != codeCompletionSettings.body + || getApiKey() != getCredential(CredentialKey.CUSTOM_SERVICE_API_KEY) + } + + fun applyChanges() { + service().state.run { + template = templateComboBox.item + chatCompletionSettings = CustomServiceChatCompletionSettingsState().apply { + url = chatCompletionsForm.url + headers = chatCompletionsForm.headers + body = chatCompletionsForm.body + } + codeCompletionSettings = CustomServiceCodeCompletionSettingsState().apply { + codeCompletionsEnabled = codeCompletionsForm.codeCompletionsEnabled + infillTemplate = codeCompletionsForm.infillTemplate + url = codeCompletionsForm.url + headers = codeCompletionsForm.headers + body = codeCompletionsForm.body + } + } + } + + fun resetForm() { + service().state.run { + templateComboBox.item = template + chatCompletionsForm.resetForm(chatCompletionSettings) + codeCompletionsForm.resetForm(codeCompletionSettings) + } + } + + private fun updateTemplateHelpTextTooltip(template: CustomServiceTemplate) { + templateHelpText.toolTipText = null + try { + HelpTooltip() + .setTitle(template.providerName) + .setBrowserLink( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.linkToDocs"), + URL(template.docsUrl) + ) + .installOn(templateHelpText) + } catch (e: MalformedURLException) { + throw RuntimeException(e) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.kt new file mode 100644 index 000000000..aa9556d61 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.kt @@ -0,0 +1,74 @@ +package ee.carlrobert.codegpt.settings.service.custom + +import com.intellij.openapi.components.* +import com.intellij.util.xmlb.annotations.OptionTag +import ee.carlrobert.codegpt.codecompletions.InfillPromptTemplate +import ee.carlrobert.codegpt.util.MapConverter + +@Service +@State( + name = "CodeGPT_CustomServiceSettings", + storages = [Storage("CodeGPT_CustomServiceSettings.xml")] +) +class CustomServiceSettings : + SimplePersistentStateComponent(CustomServiceState()) { + + override fun loadState(state: CustomServiceState) { + this.state.run { + // Migrate old settings + if (state.url != null || state.body.isNotEmpty() || state.headers.isNotEmpty()) { + template = state.template + chatCompletionSettings.url = state.url + chatCompletionSettings.body = state.body + chatCompletionSettings.headers = state.headers + url = null + body = mutableMapOf() + headers = mutableMapOf() + } + } + } +} + +class CustomServiceState : BaseState() { + var template by enum(CustomServiceTemplate.OPENAI) + var chatCompletionSettings by property(CustomServiceChatCompletionSettingsState()) + var codeCompletionSettings by property(CustomServiceCodeCompletionSettingsState()) + + @Deprecated("", ReplaceWith("this.chatCompletionSettings.url")) + var url by string() + + @Deprecated("", ReplaceWith("this.chatCompletionSettings.headers")) + var headers by map() + + @get:OptionTag(converter = MapConverter::class) + @Deprecated("", ReplaceWith("this.chatCompletionSettings.body")) + var body by map() +} + +class CustomServiceChatCompletionSettingsState : BaseState() { + var url by string(CustomServiceChatCompletionTemplate.OPENAI.url) + var headers by map() + + @get:OptionTag(converter = MapConverter::class) + var body by map() + + init { + headers.putAll(CustomServiceChatCompletionTemplate.OPENAI.headers) + body.putAll(CustomServiceChatCompletionTemplate.OPENAI.body) + } +} + +class CustomServiceCodeCompletionSettingsState : BaseState() { + var codeCompletionsEnabled by property(true) + var infillTemplate by enum(InfillPromptTemplate.OPENAI) + var url by string(CustomServiceCodeCompletionTemplate.OPENAI.url) + var headers by map() + + @get:OptionTag(converter = MapConverter::class) + var body by map() + + init { + headers.putAll(CustomServiceCodeCompletionTemplate.OPENAI.headers) + body.putAll(CustomServiceCodeCompletionTemplate.OPENAI.body) + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceTemplate.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceTemplate.kt new file mode 100644 index 000000000..2d837d4b2 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceTemplate.kt @@ -0,0 +1,69 @@ +package ee.carlrobert.codegpt.settings.service.custom + +enum class CustomServiceTemplate( + val providerName: String, + val docsUrl: String, + val chatCompletionTemplate: CustomServiceChatCompletionTemplate, + val codeCompletionTemplate: CustomServiceCodeCompletionTemplate? = null +) { + ANYSCALE( + "Anyscale", + "https://docs.endpoints.anyscale.com/", + CustomServiceChatCompletionTemplate.ANYSCALE, + CustomServiceCodeCompletionTemplate.ANYSCALE, + ), + AZURE( + "Azure OpenAI", + "https://learn.microsoft.com/en-us/azure/ai-services/openai/reference", + CustomServiceChatCompletionTemplate.AZURE, + CustomServiceCodeCompletionTemplate.AZURE + ), + DEEP_INFRA( + "DeepInfra", + "https://deepinfra.com/docs/advanced/openai_api", + CustomServiceChatCompletionTemplate.DEEP_INFRA, + CustomServiceCodeCompletionTemplate.DEEP_INFRA + ), + FIREWORKS( + "Fireworks", + "https://readme.fireworks.ai/reference/createchatcompletion", + CustomServiceChatCompletionTemplate.FIREWORKS, + CustomServiceCodeCompletionTemplate.FIREWORKS + ), + GROQ( + "Groq", + "https://docs.api.groq.com/md/openai.oas.html", + CustomServiceChatCompletionTemplate.GROQ + ), + OPENAI( + "OpenAI", + "https://platform.openai.com/docs/api-reference/chat", + CustomServiceChatCompletionTemplate.OPENAI, + CustomServiceCodeCompletionTemplate.OPENAI + ), + PERPLEXITY( + "Perplexity AI", + "https://docs.perplexity.ai/reference/post_chat_completions", + CustomServiceChatCompletionTemplate.PERPLEXITY + ), + TOGETHER( + "Together AI", + "https://docs.together.ai/docs/openai-api-compatibility", + CustomServiceChatCompletionTemplate.TOGETHER, + CustomServiceCodeCompletionTemplate.TOGETHER + ), + OLLAMA( + "Ollama", + "https://github.com/ollama/ollama/blob/main/docs/openai.md", + CustomServiceChatCompletionTemplate.OLLAMA + ), + LLAMA_CPP( + "LLaMA C/C++", + "https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md", + CustomServiceChatCompletionTemplate.LLAMA_CPP + ); + + override fun toString(): String { + return providerName + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 554d81b97..5c355356f 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -9,8 +9,8 @@ - + @@ -33,7 +33,6 @@ - diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index 2b0aa0fee..407647420 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -2,7 +2,7 @@ project.label=CodeGPT notification.group.name=CodeGPT notification group action.generateCommitMessage.title=Generate Message action.generateCommitMessage.description=Generate commit message -action.generateCommitMessage.serviceWarning=Messages can only be generated with OpenAI or Azure service +action.generateCommitMessage.serviceWarning=Messages can only be generated with OpenAI, Custom OpenAI, or Azure service action.generateCommitMessage.missingCredentials=Credentials not provided action.includeFilesInContext.title=Include In Context... action.includeFileInContext.title=Include File In Context... @@ -200,6 +200,7 @@ action.attachImage=Attach Image action.attachImageDescription=Attach an image imageFileChooser.title=Select Image imageAccordion.title=Attached image +shared.chatCompletions=Chat Completions shared.codeCompletions=Code Completions codeCompletionsForm.enableFeatureText=Enable code completions codeCompletionsForm.maxTokensLabel=Max tokens: