diff --git a/buildSrc/src/main/kotlin/codegpt.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/codegpt.java-conventions.gradle.kts index ed4f7d86..2552d55d 100644 --- a/buildSrc/src/main/kotlin/codegpt.java-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/codegpt.java-conventions.gradle.kts @@ -23,7 +23,7 @@ checkstyle { } dependencies { - implementation("ee.carlrobert:llm-client:0.4.2") + implementation("ee.carlrobert:llm-client:0.5.0") } tasks { diff --git a/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java b/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java index e6c01889..1bc1e26e 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java @@ -37,6 +37,7 @@ import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; +import okhttp3.sse.EventSource; import org.jetbrains.annotations.NotNull; public class GenerateGitCommitMessageAction extends AnAction { @@ -102,7 +103,7 @@ public class GenerateGitCommitMessageAction extends AnAction { private final StringBuilder messageBuilder = new StringBuilder(); @Override - public void onMessage(String message) { + public void onMessage(String message, EventSource eventSource) { messageBuilder.append(message); var application = ApplicationManager.getApplication(); application.invokeLater(() -> diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CallParameters.java b/src/main/java/ee/carlrobert/codegpt/completions/CallParameters.java index e4aa3137..3123da64 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CallParameters.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CallParameters.java @@ -10,6 +10,10 @@ public class CallParameters { private final Message message; private final boolean retry; + public CallParameters(Conversation conversation, Message message) { + this(conversation, ConversationType.DEFAULT, message, false); + } + public CallParameters( Conversation conversation, ConversationType conversationType, diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java index 999e3c99..8ecffe42 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java @@ -26,14 +26,9 @@ import okhttp3.OkHttpClient; public class CompletionClientProvider { public static OpenAIClient getOpenAIClient() { - var settings = OpenAISettings.getCurrentState(); - var builder = new OpenAIClient.Builder(OpenAICredentialManager.getInstance().getCredential()) - .setOrganization(settings.getOrganization()); - var baseHost = settings.getBaseHost(); - if (baseHost != null) { - builder.setHost(baseHost); - } - return builder.build(getDefaultClientBuilder()); + return new OpenAIClient.Builder(OpenAICredentialManager.getInstance().getCredential()) + .setOrganization(OpenAISettings.getCurrentState().getOrganization()) + .build(getDefaultClientBuilder()); } public static AzureClient getAzureClient() { @@ -87,7 +82,7 @@ public class CompletionClientProvider { return builder.build(getDefaultClientBuilder()); } - private static OkHttpClient.Builder getDefaultClientBuilder() { + public static OkHttpClient.Builder getDefaultClientBuilder() { OkHttpClient.Builder builder = new OkHttpClient.Builder(); var advancedSettings = AdvancedSettings.getCurrentState(); var proxyHost = advancedSettings.getProxyHost(); diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestHandler.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestHandler.java index 06e8ab4e..4a3c057f 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestHandler.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestHandler.java @@ -101,7 +101,7 @@ public class CompletionRequestHandler { } @Override - public void onMessage(String message) { + public void onMessage(String message, EventSource eventSource) { publish(message); } diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java index 7c62427f..eb92cdb8 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java @@ -5,6 +5,8 @@ import static java.lang.String.format; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import ee.carlrobert.codegpt.CodeGPTPlugin; @@ -14,10 +16,12 @@ import ee.carlrobert.codegpt.completions.llama.PromptTemplate; import ee.carlrobert.codegpt.conversations.Conversation; import ee.carlrobert.codegpt.conversations.ConversationsState; import ee.carlrobert.codegpt.conversations.message.Message; +import ee.carlrobert.codegpt.credentials.CustomServiceCredentialManager; import ee.carlrobert.codegpt.settings.GeneralSettings; import ee.carlrobert.codegpt.settings.IncludedFilesSettings; import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings; import ee.carlrobert.codegpt.settings.service.ServiceType; +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; @@ -31,11 +35,16 @@ import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionMe import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionRequest; import ee.carlrobert.llm.client.you.completion.YouCompletionRequest; import ee.carlrobert.llm.client.you.completion.YouCompletionRequestMessage; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.UUID; +import java.util.stream.Collectors; +import okhttp3.Request; +import okhttp3.RequestBody; import org.jetbrains.annotations.Nullable; public class CompletionRequestProvider { @@ -247,4 +256,51 @@ public class CompletionRequestProvider { return messages.stream().filter(Objects::nonNull).collect(toList()); } + + public Request buildCustomOpenAIChatCompletionRequest( + CustomServiceState customConfiguration, + CallParameters callParameters) { + var requestBuilder = new Request.Builder().url(customConfiguration.getUrl().trim()); + for (var entry : customConfiguration.getHeaders().entrySet()) { + String value = entry.getValue(); + if (value.contains("$CUSTOM_SERVICE_API_KEY")) { + value = value.replace("$CUSTOM_SERVICE_API_KEY", + CustomServiceCredentialManager.getInstance().getCredential()); + } + requestBuilder.addHeader(entry.getKey(), value); + } + + var body = customConfiguration.getBody().entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> processEntryValue(entry.getValue(), callParameters) + )); + + try { + var requestBody = RequestBody.create(new ObjectMapper() + .writerWithDefaultPrettyPrinter() + .writeValueAsString(body) + .getBytes(StandardCharsets.UTF_8)); + return requestBuilder.post(requestBody).build(); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private Object processEntryValue(Object value, CallParameters callParameters) { + if (!(value instanceof String)) { + return value; + } + + String stringValue = (String) value; + switch (stringValue.toLowerCase().trim()) { + case "$openai_messages": + return buildMessages(callParameters, false); + case "true": + case "false": + return Boolean.parseBoolean(stringValue); + default: + return value; + } + } } diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java index 7a832491..cb11e44d 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java @@ -16,15 +16,19 @@ import ee.carlrobert.codegpt.credentials.OpenAICredentialManager; import ee.carlrobert.codegpt.settings.GeneralSettings; import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings; import ee.carlrobert.codegpt.settings.service.azure.AzureSettings; +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.llm.client.llama.completion.LlamaCompletionRequest; +import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionEventSourceListener; import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionMessage; import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionRequest; import ee.carlrobert.llm.completion.CompletionEventListener; import java.util.List; import java.util.Optional; +import okhttp3.Request; import okhttp3.sse.EventSource; +import okhttp3.sse.EventSources; @Service public final class CompletionRequestService { @@ -36,6 +40,15 @@ public final class CompletionRequestService { return ApplicationManager.getApplication().getService(CompletionRequestService.class); } + public EventSource getCustomOpenAIChatCompletionAsync( + Request customRequest, + CompletionEventListener eventListener) { + var httpClient = CompletionClientProvider.getDefaultClientBuilder().build(); + return EventSources.createFactory(httpClient).newEventSource( + customRequest, + new OpenAIChatCompletionEventSourceListener(eventListener)); + } + public EventSource getChatCompletionAsync( CallParameters callParameters, boolean useContextualSearch, @@ -44,13 +57,19 @@ public final class CompletionRequestService { switch (GeneralSettings.getCurrentState().getSelectedService()) { case OPENAI: var openAISettings = OpenAISettings.getCurrentState(); - var customModel = openAISettings.getCustomModel(); return CompletionClientProvider.getOpenAIClient().getChatCompletionAsync( requestProvider.buildOpenAIChatCompletionRequest( - customModel.trim().isEmpty() ? openAISettings.getModel() : customModel, + openAISettings.getModel(), callParameters, useContextualSearch, - openAISettings.isUsingCustomPath() ? openAISettings.getPath() : null), + null), + eventListener); + case CUSTOM_OPENAI: + var customConfiguration = CustomServiceSettings.getCurrentState(); + return getCustomOpenAIChatCompletionAsync( + requestProvider.buildCustomOpenAIChatCompletionRequest( + customConfiguration, + callParameters), eventListener); case AZURE: var azureSettings = AzureSettings.getCurrentState(); diff --git a/src/main/java/ee/carlrobert/codegpt/conversations/ConversationService.java b/src/main/java/ee/carlrobert/codegpt/conversations/ConversationService.java index c5ec8641..ac2e5a7c 100644 --- a/src/main/java/ee/carlrobert/codegpt/conversations/ConversationService.java +++ b/src/main/java/ee/carlrobert/codegpt/conversations/ConversationService.java @@ -190,6 +190,8 @@ public final class ConversationService { switch (serviceType) { case OPENAI: return OpenAISettings.getCurrentState().getModel(); + case CUSTOM_OPENAI: + return "CustomService"; case AZURE: return AzureSettings.getCurrentState().getDeploymentId(); case YOU: diff --git a/src/main/java/ee/carlrobert/codegpt/credentials/CustomServiceCredentialManager.java b/src/main/java/ee/carlrobert/codegpt/credentials/CustomServiceCredentialManager.java new file mode 100644 index 00000000..9b0f9dab --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/credentials/CustomServiceCredentialManager.java @@ -0,0 +1,16 @@ +package ee.carlrobert.codegpt.credentials; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.components.Service; + +@Service +public final class CustomServiceCredentialManager extends SingleCredentialManager { + + private CustomServiceCredentialManager() { + super("CUSTOM_SERVICE_API_KEY"); + } + + public static CustomServiceCredentialManager getInstance() { + return ApplicationManager.getApplication().getService(CustomServiceCredentialManager.class); + } +} diff --git a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsComponent.java b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsComponent.java index 34215cd3..9943daaa 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsComponent.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsComponent.java @@ -1,6 +1,7 @@ package ee.carlrobert.codegpt.settings; import static ee.carlrobert.codegpt.settings.service.ServiceType.AZURE; +import static ee.carlrobert.codegpt.settings.service.ServiceType.CUSTOM_OPENAI; import static ee.carlrobert.codegpt.settings.service.ServiceType.LLAMA_CPP; import static ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI; import static ee.carlrobert.codegpt.settings.service.ServiceType.YOU; @@ -8,13 +9,16 @@ import static java.util.stream.Collectors.toList; import com.intellij.openapi.Disposable; import com.intellij.openapi.ui.ComboBox; -import com.intellij.openapi.util.SystemInfoRt; 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 java.awt.CardLayout; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Insets; import java.util.Arrays; import javax.swing.DefaultComboBoxModel; import javax.swing.JComponent; @@ -30,9 +34,12 @@ public class GeneralSettingsComponent { public GeneralSettingsComponent(Disposable parentDisposable, GeneralSettings settings) { displayNameField = new JBTextField(settings.getState().getDisplayName(), 20); serviceSelectionForm = new ServiceSelectionForm(parentDisposable); - var cardLayout = new CardLayout(); + 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.getAzureSettingsForm().getForm(), AZURE.getCode()); cards.add(serviceSelectionForm.getYouSettingsForm(), YOU.getCode()); cards.add(serviceSelectionForm.getLlamaSettingsForm(), LLAMA_CPP.getCode()); @@ -83,4 +90,29 @@ public class GeneralSettingsComponent { public void setDisplayName(String displayName) { displayNameField.setText(displayName); } + + static class DynamicCardLayout extends CardLayout { + + @Override + public Dimension preferredLayoutSize(Container parent) { + Component current = findVisibleComponent(parent); + if (current != null) { + Insets insets = parent.getInsets(); + Dimension preferredSize = current.getPreferredSize(); + preferredSize.width += insets.left + insets.right; + preferredSize.height += insets.top + insets.bottom; + return preferredSize; + } + return super.preferredLayoutSize(parent); + } + + private Component findVisibleComponent(Container parent) { + for (Component comp : parent.getComponents()) { + if (comp.isVisible()) { + return comp; + } + } + return null; + } + } } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java index f7296573..51d00568 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java @@ -6,10 +6,13 @@ import com.intellij.openapi.util.Disposer; import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.conversations.ConversationsState; import ee.carlrobert.codegpt.credentials.AzureCredentialsManager; +import ee.carlrobert.codegpt.credentials.CustomServiceCredentialManager; import ee.carlrobert.codegpt.credentials.LlamaCredentialManager; import ee.carlrobert.codegpt.credentials.OpenAICredentialManager; 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; @@ -56,6 +59,8 @@ public class GeneralSettingsConfigurable implements Configurable { return !component.getDisplayName().equals(settings.getDisplayName()) || component.getSelectedService() != settings.getSelectedService() || OpenAISettings.getInstance().isModified(serviceSelectionForm.getOpenAISettingsForm()) + || CustomServiceSettings.getInstance() + .isModified(serviceSelectionForm.getCustomConfigurationSettingsForm()) || AzureSettings.getInstance().isModified(serviceSelectionForm.getAzureSettingsForm()) || YouSettings.getInstance().isModified(serviceSelectionForm.getYouSettingsForm()) || LlamaSettings.getInstance().isModified(serviceSelectionForm.getLlamaSettingsForm()); @@ -70,6 +75,7 @@ public class GeneralSettingsConfigurable implements Configurable { var serviceSelectionForm = component.getServiceSelectionForm(); var openAISettingsForm = serviceSelectionForm.getOpenAISettingsForm(); applyOpenAISettings(openAISettingsForm); + applyCustomOpenAISettings(serviceSelectionForm.getCustomConfigurationSettingsForm()); applyAzureSettings(serviceSelectionForm.getAzureSettingsForm()); applyYouSettings(serviceSelectionForm.getYouSettingsForm()); applyLlamaSettings(serviceSelectionForm.getLlamaSettingsForm()); @@ -87,6 +93,16 @@ public class GeneralSettingsConfigurable implements Configurable { } } + private void applyOpenAISettings(OpenAISettingsForm form) { + OpenAICredentialManager.getInstance().setCredential(form.getApiKey()); + OpenAISettings.getInstance().loadState(form.getCurrentState()); + } + + private void applyCustomOpenAISettings(CustomServiceForm form) { + CustomServiceCredentialManager.getInstance().setCredential(form.getApiKey()); + CustomServiceSettings.getInstance().loadState(form.getCurrentState()); + } + private void applyLlamaSettings(LlamaSettingsForm form) { LlamaCredentialManager.getInstance() .setCredential(form.getLlamaServerPreferencesForm().getApiKey()); @@ -109,12 +125,7 @@ public class GeneralSettingsConfigurable implements Configurable { var settings = GeneralSettings.getCurrentState(); component.setDisplayName(settings.getDisplayName()); component.setSelectedService(settings.getSelectedService()); - - var serviceSelectionForm = component.getServiceSelectionForm(); - serviceSelectionForm.getOpenAISettingsForm().resetForm(); - serviceSelectionForm.getAzureSettingsForm().resetForm(); - serviceSelectionForm.getLlamaSettingsForm().resetForm(); - serviceSelectionForm.getYouSettingsForm().resetForm(); + component.getServiceSelectionForm().resetForms(); } @Override @@ -125,11 +136,6 @@ public class GeneralSettingsConfigurable implements Configurable { component = null; } - private void applyOpenAISettings(OpenAISettingsForm form) { - OpenAICredentialManager.getInstance().setCredential(form.getApiKey()); - OpenAISettings.getInstance().loadState(form.getCurrentState()); - } - private void resetActiveTab() { ConversationsState.getInstance().setCurrentConversation(null); var project = ApplicationUtil.findCurrentProject(); diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceSelectionForm.java b/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceSelectionForm.java index e30dbbf9..28643ab2 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceSelectionForm.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceSelectionForm.java @@ -3,23 +3,27 @@ package ee.carlrobert.codegpt.settings.service; import com.intellij.openapi.Disposable; 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; -import ee.carlrobert.codegpt.settings.service.you.YouSettingsState; public class ServiceSelectionForm { private final OpenAISettingsForm openAISettingsForm; + private final CustomServiceForm customServiceForm; 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()); azureSettingsForm = new AzureSettingsForm(AzureSettings.getCurrentState()); youSettingsForm = new YouSettingsForm(YouSettings.getCurrentState(), parentDisposable); llamaSettingsForm = new LlamaSettingsForm(LlamaSettings.getCurrentState()); @@ -29,6 +33,10 @@ public class ServiceSelectionForm { return openAISettingsForm; } + public CustomServiceForm getCustomConfigurationSettingsForm() { + return customServiceForm; + } + public AzureSettingsForm getAzureSettingsForm() { return azureSettingsForm; } @@ -40,4 +48,12 @@ public class ServiceSelectionForm { public LlamaSettingsForm getLlamaSettingsForm() { return llamaSettingsForm; } + + public void resetForms() { + openAISettingsForm.resetForm(); + customServiceForm.resetForm(); + azureSettingsForm.resetForm(); + youSettingsForm.resetForm(); + llamaSettingsForm.resetForm(); + } } 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 ec83a57a..0b31d65b 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceType.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceType.java @@ -3,18 +3,19 @@ package ee.carlrobert.codegpt.settings.service; import ee.carlrobert.codegpt.CodeGPTBundle; public enum ServiceType { - OPENAI("OPENAI", CodeGPTBundle.get("service.openai.title"), "chat.completion"), - AZURE("AZURE", CodeGPTBundle.get("service.azure.title"), "azure.chat.completion"), - YOU("YOU", CodeGPTBundle.get("service.you.title"), "you.chat.completion"), - LLAMA_CPP("LLAMA_CPP", CodeGPTBundle.get("service.llama.title"), "llama.chat.completion"); + OPENAI("OPENAI", "service.openai.title", "chat.completion"), + CUSTOM_OPENAI("CUSTOM_OPENAI", "service.custom.openai.title", "custom.openai.chat.completion"), + AZURE("AZURE", "service.azure.title", "azure.chat.completion"), + YOU("YOU", "service.you.title", "you.chat.completion"), + LLAMA_CPP("LLAMA_CPP", "service.llama.title", "llama.chat.completion"); private final String code; private final String label; private final String completionCode; - ServiceType(String code, String label, String completionCode) { + ServiceType(String code, String messageKey, String completionCode) { this.code = code; - this.label = label; + this.label = CodeGPTBundle.get(messageKey); this.completionCode = completionCode; } 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 new file mode 100644 index 00000000..7070f5f2 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceForm.java @@ -0,0 +1,175 @@ +package ee.carlrobert.codegpt.settings.service.custom; + +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.CustomServiceCredentialManager; +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.annotation.Nullable; +import javax.swing.Box; +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import okhttp3.sse.EventSource; + +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(CustomServiceState settings) { + apiKeyField = new JBPasswordField(); + apiKeyField.setColumns(30); + apiKeyField.setText(CustomServiceCredentialManager.getInstance().getCredential()); + 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("settingsConfigurable.service.openai.configuration.title"))) + .addComponent(withEmptyLeftBorder(form)) + .addComponentFillVertically(new JPanel(), 0) + .getPanel(); + } + + public @Nullable String getApiKey() { + var apiKey = new String(apiKeyField.getPassword()); + return apiKey.isEmpty() ? null : apiKey; + } + + public CustomServiceState getCurrentState() { + var state = new CustomServiceState(); + 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(CustomServiceCredentialManager.getInstance().getCredential()); + 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(CustomServiceState 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 new file mode 100644 index 00000000..0d7e5d13 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceFormTabbedPane.java @@ -0,0 +1,94 @@ +package ee.carlrobert.codegpt.settings.service.custom; + +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; + +import com.intellij.ui.ToolbarDecorator; +import com.intellij.ui.components.JBTabbedPane; +import com.intellij.ui.table.JBTable; +import com.intellij.util.ui.JBUI; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import javax.swing.JPanel; +import javax.swing.table.DefaultTableModel; + +class CustomServiceFormTabbedPane extends JBTabbedPane { + + private final JBTable headersTable; + private final JBTable bodyTable; + + CustomServiceFormTabbedPane(CustomServiceState customConfiguration) { + headersTable = new JBTable( + new DefaultTableModel(toArray(customConfiguration.getHeaders()), + new Object[]{"Key", "Value"})); + bodyTable = new JBTable( + new DefaultTableModel(toArray(customConfiguration.getBody()), + new Object[]{"Key", "Value"})); + + setTabComponentInsets(JBUI.insetsTop(8)); + addTab("Headers", createTablePanel(headersTable)); + addTab("Body", createTablePanel(bodyTable)); + } + + public void setEnabled(boolean enabled) { + headersTable.setEnabled(enabled); + bodyTable.setEnabled(enabled); + } + + public void setHeaders(Map headers) { + setTableData(headersTable, headers); + } + + public Map getHeaders() { + return getTableData(headersTable).entrySet().stream() + .filter(entry -> entry.getKey() != null && entry.getValue() != null) + .collect(toMap(Entry::getKey, entry -> (String) entry.getValue())); + } + + public void setBody(Map body) { + setTableData(bodyTable, body); + } + + public Map getBody() { + return getTableData(bodyTable); + } + + private void setTableData(JBTable table, Map values) { + DefaultTableModel model = (DefaultTableModel) table.getModel(); + model.setRowCount(0); + + for (var entry : values.entrySet()) { + model.addRow(new Object[]{entry.getKey(), entry.getValue()}); + } + } + + private Map getTableData(JBTable table) { + var model = (DefaultTableModel) table.getModel(); + var data = new HashMap(); + for (int i = 0; i < model.getRowCount(); i++) { + var key = (String) model.getValueAt(i, 0); + data.put(key, model.getValueAt(i, 1)); + } + return data; + } + + public static Object[][] toArray(Map actionsMap) { + return actionsMap.entrySet() + .stream() + .map((entry) -> new Object[]{entry.getKey(), entry.getValue()}) + .collect(toList()) + .toArray(new Object[0][0]); + } + + private JPanel createTablePanel(JBTable table) { + return ToolbarDecorator.createDecorator(table) + .setAddAction(anActionButton -> + ((DefaultTableModel) table.getModel()).addRow(new Object[]{"", null})) + .setRemoveAction(anActionButton -> + ((DefaultTableModel) table.getModel()).removeRow(table.getSelectedRow())) + .disableUpAction() + .disableDownAction() + .createPanel(); + } +} 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 new file mode 100644 index 00000000..0a60825f --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.java @@ -0,0 +1,43 @@ +package ee.carlrobert.codegpt.settings.service.custom; + +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.CustomServiceCredentialManager; +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 CustomServiceState state = new CustomServiceState(); + + @Override + @NotNull + public CustomServiceState getState() { + return state; + } + + @Override + public void loadState(@NotNull CustomServiceState state) { + this.state = state; + } + + public static CustomServiceState 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(), + CustomServiceCredentialManager.getInstance().getCredential()); + } +} \ No newline at end of file diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceState.java b/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceState.java new file mode 100644 index 00000000..393a21d1 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceState.java @@ -0,0 +1,66 @@ +package ee.carlrobert.codegpt.settings.service.custom; + +import static ee.carlrobert.codegpt.settings.service.custom.CustomServiceTemplate.OPENAI; + +import java.util.Map; +import java.util.Objects; + +public class CustomServiceState { + + private String url = OPENAI.getUrl(); + private Map headers = OPENAI.getHeaders(); + 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; + } + CustomServiceState that = (CustomServiceState) 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 new file mode 100644 index 00000000..49226a4c --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceTemplate.java @@ -0,0 +1,158 @@ +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/settings/service/openai/OpenAISettingsForm.java b/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsForm.java index 34c53a25..d18047b1 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsForm.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsForm.java @@ -3,7 +3,6 @@ package ee.carlrobert.codegpt.settings.service.openai; import static ee.carlrobert.codegpt.ui.UIUtil.withEmptyLeftBorder; import com.intellij.openapi.ui.ComboBox; -import com.intellij.ui.DocumentAdapter; import com.intellij.ui.EnumComboBoxModel; import com.intellij.ui.TitledSeparator; import com.intellij.ui.components.JBPasswordField; @@ -16,15 +15,10 @@ import ee.carlrobert.codegpt.ui.UIUtil; import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel; import javax.annotation.Nullable; import javax.swing.JPanel; -import javax.swing.event.DocumentEvent; -import org.jetbrains.annotations.NotNull; public class OpenAISettingsForm { private final JBPasswordField apiKeyField; - private final JBTextField customModelField; - private final JBTextField baseHostField; - private final JBTextField pathField; private final JBTextField organizationField; private final ComboBox completionModelComboBox; @@ -32,64 +26,34 @@ public class OpenAISettingsForm { apiKeyField = new JBPasswordField(); apiKeyField.setColumns(30); apiKeyField.setText(OpenAICredentialManager.getInstance().getCredential()); + organizationField = new JBTextField(settings.getOrganization(), 30); completionModelComboBox = new ComboBox<>( new EnumComboBoxModel<>(OpenAIChatCompletionModel.class)); - completionModelComboBox.setEnabled(settings.getCustomModel().isEmpty()); completionModelComboBox.setSelectedItem( OpenAIChatCompletionModel.findByCode(settings.getModel())); - customModelField = new JBTextField(settings.getCustomModel(), 20); - customModelField.getDocument().addDocumentListener(new DocumentAdapter() { - @Override - protected void textChanged(@NotNull DocumentEvent e) { - completionModelComboBox.setEnabled(customModelField.getText().isEmpty()); - } - }); - baseHostField = new JBTextField(settings.getBaseHost(), 30); - pathField = new JBTextField(settings.getPath(), 30); - organizationField = new JBTextField(settings.getOrganization(), 30); } public JPanel getForm() { - var requestConfigurationPanel = UI.PanelFactory.grid() - .add(UI.PanelFactory.panel(completionModelComboBox) - .withLabel(CodeGPTBundle.get( - "settingsConfigurable.shared.model.label")) - .resizeX(false)) - .add(UI.PanelFactory.panel(customModelField) - .withLabel(CodeGPTBundle.get( - "settingsConfigurable.service.openai.customModel.label")) - .resizeX(false)) + var configurationGrid = UI.PanelFactory.grid() + .add(UI.PanelFactory.panel(apiKeyField) + .withLabel(CodeGPTBundle.get("settingsConfigurable.shared.apiKey.label")) + .resizeX(false) + .withComment(CodeGPTBundle.get("settingsConfigurable.service.openai.apiKey.comment")) + .withCommentHyperlinkListener(UIUtil::handleHyperlinkClicked)) .add(UI.PanelFactory.panel(organizationField) - .withLabel(CodeGPTBundle.get( - "settingsConfigurable.service.openai.organization.label")) + .withLabel(CodeGPTBundle.get("settingsConfigurable.service.openai.organization.label")) .resizeX(false) .withComment(CodeGPTBundle.get( "settingsConfigurable.section.openai.organization.comment"))) - .add(UI.PanelFactory.panel(baseHostField) - .withLabel(CodeGPTBundle.get( - "settingsConfigurable.shared.baseHost.label")) + .add(UI.PanelFactory.panel(completionModelComboBox) + .withLabel(CodeGPTBundle.get("settingsConfigurable.shared.model.label")) .resizeX(false)) - .add(UI.PanelFactory.panel(pathField) - .withLabel(CodeGPTBundle.get( - "settingsConfigurable.shared.path.label")) - .resizeX(false)) - .createPanel(); - - var apiKeyFieldPanel = UI.PanelFactory.panel(apiKeyField) - .withLabel(CodeGPTBundle.get("settingsConfigurable.shared.apiKey.label")) - .resizeX(false) - .withComment( - CodeGPTBundle.get("settingsConfigurable.service.openai.apiKey.comment")) - .withCommentHyperlinkListener(UIUtil::handleHyperlinkClicked) .createPanel(); return FormBuilder.createFormBuilder() .addComponent(new TitledSeparator( - CodeGPTBundle.get("settingsConfigurable.shared.authentication.title"))) - .addComponent(withEmptyLeftBorder(apiKeyFieldPanel)) - .addComponent(new TitledSeparator( - CodeGPTBundle.get("settingsConfigurable.shared.requestConfiguration.title"))) - .addComponent(withEmptyLeftBorder(requestConfigurationPanel)) + CodeGPTBundle.get("settingsConfigurable.service.openai.configuration.title"))) + .addComponent(withEmptyLeftBorder(configurationGrid)) .addComponentFillVertically(new JPanel(), 0) .getPanel(); } @@ -108,10 +72,7 @@ public class OpenAISettingsForm { public OpenAISettingsState getCurrentState() { var state = new OpenAISettingsState(); state.setModel(getModel()); - state.setCustomModel(customModelField.getText()); state.setOrganization(organizationField.getText()); - state.setBaseHost(baseHostField.getText()); - state.setPath(pathField.getText()); return state; } @@ -120,9 +81,6 @@ public class OpenAISettingsForm { apiKeyField.setText(OpenAICredentialManager.getInstance().getCredential()); completionModelComboBox.setSelectedItem( OpenAIChatCompletionModel.findByCode(state.getModel())); - customModelField.setText(state.getCustomModel()); organizationField.setText(state.getOrganization()); - baseHostField.setText(state.getBaseHost()); - pathField.setText(state.getPath()); } } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsState.java b/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsState.java index 975e2553..75134d65 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsState.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsState.java @@ -5,17 +5,8 @@ import java.util.Objects; public class OpenAISettingsState { - private static final String BASE_PATH = "/v1/chat/completions"; - private String organization = ""; - private String baseHost = "https://api.openai.com"; - private String path = BASE_PATH; - private String model = OpenAIChatCompletionModel.GPT_3_5.getCode(); - private String customModel = ""; - - public boolean isUsingCustomPath() { - return !BASE_PATH.equals(path); - } + private String model = OpenAIChatCompletionModel.GPT_3_5_0125_16k.getCode(); public String getOrganization() { return organization; @@ -25,22 +16,6 @@ public class OpenAISettingsState { this.organization = organization; } - public String getBaseHost() { - return baseHost; - } - - public void setBaseHost(String openAIBaseHost) { - this.baseHost = openAIBaseHost; - } - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - public String getModel() { return model; } @@ -49,14 +24,6 @@ public class OpenAISettingsState { this.model = model; } - public String getCustomModel() { - return customModel; - } - - public void setCustomModel(String customModel) { - this.customModel = customModel; - } - @Override public boolean equals(Object o) { if (this == o) { @@ -66,15 +33,11 @@ public class OpenAISettingsState { return false; } OpenAISettingsState that = (OpenAISettingsState) o; - return Objects.equals(organization, that.organization) - && Objects.equals(baseHost, that.baseHost) - && Objects.equals(path, that.path) - && Objects.equals(model, that.model) - && Objects.equals(customModel, that.customModel); + return Objects.equals(organization, that.organization) && Objects.equals(model, that.model); } @Override public int hashCode() { - return Objects.hash(organization, baseHost, path, model, customModel); + return Objects.hash(organization, model); } } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/you/YouSettings.java b/src/main/java/ee/carlrobert/codegpt/settings/service/you/YouSettings.java index 1e45d007..0293d127 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/you/YouSettings.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/you/YouSettings.java @@ -4,8 +4,6 @@ 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.YouCredentialManager; -import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; @State(name = "CodeGPT_YouSettings", storages = @Storage("CodeGPT_YouSettings.xml")) @@ -33,8 +31,6 @@ public class YouSettings implements PersistentStateComponent { } public boolean isModified(YouSettingsForm form) { - var password = YouCredentialManager.getInstance().getCredential(); - return !form.getCurrentState().equals(state) - || (!form.getEmail().isEmpty() && !StringUtils.equals(form.getPassword(), password)); + return !form.getCurrentState().equals(state); } } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/ModelComboBoxAction.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java similarity index 82% rename from src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/ModelComboBoxAction.java rename to src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java index f6df91cc..7c6c71e2 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/ModelComboBoxAction.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java @@ -1,5 +1,7 @@ -package ee.carlrobert.codegpt.toolwindow.chat.standard; +package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea; +import static ee.carlrobert.codegpt.settings.service.ServiceType.CUSTOM_OPENAI; +import static ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI; import static java.lang.String.format; import com.intellij.openapi.actionSystem.AnAction; @@ -15,6 +17,7 @@ import ee.carlrobert.codegpt.conversations.ConversationsState; 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.custom.CustomServiceSettings; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettingsState; @@ -56,22 +59,19 @@ public class ModelComboBoxAction extends ComboBoxAction { var presentation = ((ComboBoxButton) button).getPresentation(); var actionGroup = new DefaultActionGroup(); actionGroup.addSeparator("OpenAI"); - var settings = OpenAISettings.getCurrentState(); - if (settings.getCustomModel().isEmpty()) { - List.of( - OpenAIChatCompletionModel.GPT_4_0125_128k, - OpenAIChatCompletionModel.GPT_3_5_0125_16k, - OpenAIChatCompletionModel.GPT_4_32k, - OpenAIChatCompletionModel.GPT_4, - OpenAIChatCompletionModel.GPT_3_5) - .forEach(model -> actionGroup.add(createOpenAIModelAction(model, presentation))); - } else { - actionGroup.add(createModelAction( - ServiceType.OPENAI, - settings.getCustomModel(), - Icons.OpenAI, - presentation)); - } + List.of( + OpenAIChatCompletionModel.GPT_4_0125_128k, + OpenAIChatCompletionModel.GPT_3_5_0125_16k, + OpenAIChatCompletionModel.GPT_4_32k, + OpenAIChatCompletionModel.GPT_4, + OpenAIChatCompletionModel.GPT_3_5) + .forEach(model -> actionGroup.add(createOpenAIModelAction(model, presentation))); + actionGroup.addSeparator("Custom OpenAI Service"); + actionGroup.add(createModelAction( + CUSTOM_OPENAI, + CustomServiceSettings.getCurrentState().getTemplate().getName(), + Icons.OpenAI, + presentation)); actionGroup.addSeparator(); actionGroup.add( createModelAction(ServiceType.AZURE, "Azure OpenAI", Icons.Azure, presentation)); @@ -96,7 +96,14 @@ public class ModelComboBoxAction extends ComboBoxAction { switch (selectedService) { case OPENAI: templatePresentation.setIcon(Icons.OpenAI); - templatePresentation.setText(getOpenAiPresentationText()); + templatePresentation.setText( + OpenAIChatCompletionModel.findByCode(openAISettings.getModel()).getDescription()); + break; + case CUSTOM_OPENAI: + templatePresentation.setIcon(Icons.OpenAI); + templatePresentation.setText(CustomServiceSettings.getCurrentState() + .getTemplate() + .getName()); break; case AZURE: templatePresentation.setIcon(Icons.Azure); @@ -114,14 +121,6 @@ public class ModelComboBoxAction extends ComboBoxAction { } } - private String getOpenAiPresentationText() { - var settings = OpenAISettings.getCurrentState(); - if (settings.getCustomModel().isEmpty()) { - return OpenAIChatCompletionModel.findByCode(openAISettings.getModel()).getDescription(); - } - return settings.getCustomModel(); - } - private String getLlamaCppPresentationText() { var llamaSettingState = LlamaSettings.getCurrentState(); if (!llamaSettingState.isRunLocalServer()) { @@ -178,7 +177,7 @@ public class ModelComboBoxAction extends ComboBoxAction { private AnAction createOpenAIModelAction( OpenAIChatCompletionModel model, Presentation comboBoxPresentation) { - createModelAction(ServiceType.OPENAI, model.getDescription(), Icons.OpenAI, + createModelAction(OPENAI, model.getDescription(), Icons.OpenAI, comboBoxPresentation); return new DumbAwareAction(model.getDescription(), "", Icons.OpenAI) { @Override @@ -191,7 +190,7 @@ public class ModelComboBoxAction extends ComboBoxAction { public void actionPerformed(@NotNull AnActionEvent e) { openAISettings.setModel(model.getCode()); handleProviderChange( - ServiceType.OPENAI, + OPENAI, model.getDescription(), Icons.OpenAI, comboBoxPresentation); diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextAreaHeader.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextAreaHeader.java index 6ce36373..5591ed99 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextAreaHeader.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextAreaHeader.java @@ -8,7 +8,6 @@ import com.intellij.util.ui.JBUI; import ee.carlrobert.codegpt.completions.you.YouSubscriptionNotifier; import ee.carlrobert.codegpt.completions.you.auth.SignedOutNotifier; import ee.carlrobert.codegpt.settings.service.ServiceType; -import ee.carlrobert.codegpt.toolwindow.chat.standard.ModelComboBoxAction; import java.awt.BorderLayout; import javax.swing.JPanel; diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 758d3c2c..b03f2d95 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -30,6 +30,7 @@ + diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index 0cf3c608..a90b1be0 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -17,6 +17,8 @@ settings.displayName=CodeGPT: Settings settings.openaiQuotaExceeded=OpenAI quota exceeded. settingsConfigurable.displayName.label=Display name: settingsConfigurable.service.label=Service: +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.configuration.title=Configuration settingsConfigurable.service.openai.apiKey.comment=You can find your Secret API key in your User settings. settingsConfigurable.service.openai.customModel.label=Custom model: settingsConfigurable.service.openai.organization.label=Organization: @@ -103,6 +105,12 @@ settingsConfigurable.service.llama.minP.label=Min P: settingsConfigurable.service.llama.minP.comment=Sets a minimum base probability threshold for token selection (default: 0.05) settingsConfigurable.service.llama.repeatPenalty.label=Repeat penalty: settingsConfigurable.service.llama.repeatPenalty.comment=Control the repetition of token sequences in the generated text (default: 1.1) +settingsConfigurable.service.custom.openai.testConnection.label=Test Connection +settingsConfigurable.service.custom.openai.presetTemplate.label=Preset template: +settingsConfigurable.service.custom.openai.url.label=URL: +settingsConfigurable.service.custom.openai.linkToDocs=Link to API docs +settingsConfigurable.service.custom.openai.connectionSuccess=Connection successful. +settingsConfigurable.service.custom.openai.connectionFailed=Connection failed. configurationConfigurable.section.commitMessage.title=Commit Message configurationConfigurable.section.commitMessage.systemPromptField.label=Prompt: configurationConfigurable.section.commitMessage.systemPromptField.comment=Custom system prompt used for commit message generation. @@ -156,6 +164,7 @@ 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.openai.title=OpenAI Service +service.custom.openai.title=Custom OpenAI Service service.azure.title=Azure Service service.you.title=You.com Service (Free, Cloud) service.llama.title=LLaMA C/C++ Port (Free, Local) diff --git a/src/test/java/ee/carlrobert/codegpt/completions/CompletionRequestProviderTest.java b/src/test/java/ee/carlrobert/codegpt/completions/CompletionRequestProviderTest.java index 74b9e91a..9b6872ad 100644 --- a/src/test/java/ee/carlrobert/codegpt/completions/CompletionRequestProviderTest.java +++ b/src/test/java/ee/carlrobert/codegpt/completions/CompletionRequestProviderTest.java @@ -13,7 +13,6 @@ import ee.carlrobert.codegpt.conversations.ConversationService; import ee.carlrobert.codegpt.conversations.message.Message; import ee.carlrobert.codegpt.credentials.OpenAICredentialManager; import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings; -import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; import ee.carlrobert.llm.client.http.ResponseEntity; import ee.carlrobert.llm.client.http.exchange.BasicHttpExchange; import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel; @@ -25,7 +24,6 @@ public class CompletionRequestProviderTest extends IntegrationTest { public void testChatCompletionRequestWithSystemPromptOverride() { OpenAICredentialManager.getInstance().setCredential("TEST_API_KEY"); - OpenAISettings.getCurrentState().setBaseHost(null); ConfigurationSettings.getCurrentState().setSystemPrompt("TEST_SYSTEM_PROMPT"); var conversation = ConversationService.getInstance().startConversation(); var firstMessage = createDummyMessage(500); diff --git a/src/test/java/testsupport/mixin/ShortcutsTestMixin.java b/src/test/java/testsupport/mixin/ShortcutsTestMixin.java index b8367f8b..620f3c12 100644 --- a/src/test/java/testsupport/mixin/ShortcutsTestMixin.java +++ b/src/test/java/testsupport/mixin/ShortcutsTestMixin.java @@ -14,7 +14,6 @@ public interface ShortcutsTestMixin { GeneralSettings.getCurrentState().setSelectedService(ServiceType.OPENAI); OpenAICredentialManager.getInstance().setCredential("TEST_API_KEY"); OpenAISettings.getCurrentState().setModel("gpt-4"); - OpenAISettings.getCurrentState().setBaseHost(null); } default void useAzureService() {