diff --git a/src/main/java/ee/carlrobert/codegpt/Icons.java b/src/main/java/ee/carlrobert/codegpt/Icons.java index 0e210996..89467c2c 100644 --- a/src/main/java/ee/carlrobert/codegpt/Icons.java +++ b/src/main/java/ee/carlrobert/codegpt/Icons.java @@ -10,6 +10,7 @@ public final class Icons { IconLoader.getIcon("/icons/codegpt-small.svg", Icons.class); public static final Icon Anthropic = IconLoader.getIcon("/icons/anthropic.svg", Icons.class); public static final Icon Azure = IconLoader.getIcon("/icons/azure.svg", Icons.class); + public static final Icon Google = IconLoader.getIcon("/icons/google.svg", Icons.class); public static final Icon Llama = IconLoader.getIcon("/icons/llama.svg", Icons.class); public static final Icon OpenAI = IconLoader.getIcon("/icons/openai.svg", Icons.class); public static final Icon Send = IconLoader.getIcon("/icons/send.svg", Icons.class); diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java index 0e74e9bd..40435aa0 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java @@ -14,6 +14,7 @@ import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; import ee.carlrobert.llm.client.anthropic.ClaudeClient; import ee.carlrobert.llm.client.azure.AzureClient; import ee.carlrobert.llm.client.azure.AzureCompletionRequestParams; +import ee.carlrobert.llm.client.google.GoogleClient; import ee.carlrobert.llm.client.llama.LlamaClient; import ee.carlrobert.llm.client.ollama.OllamaClient; import ee.carlrobert.llm.client.openai.OpenAIClient; @@ -105,6 +106,12 @@ public class CompletionClientProvider { .build(getDefaultClientBuilder()); } + + public static GoogleClient getGoogleClient() { + return new GoogleClient.Builder(getCredential(CredentialKey.GOOGLE_API_KEY)) + .build(getDefaultClientBuilder()); + } + public static OkHttpClient.Builder getDefaultClientBuilder() { OkHttpClient.Builder builder = new OkHttpClient.Builder(); var advancedSettings = AdvancedSettings.getCurrentState(); diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java index 04f0d39c..9742b245 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java @@ -40,6 +40,12 @@ import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionRequest; import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionStandardMessage; import ee.carlrobert.llm.client.anthropic.completion.ClaudeMessageImageContent; import ee.carlrobert.llm.client.anthropic.completion.ClaudeMessageTextContent; +import ee.carlrobert.llm.client.google.completion.GoogleCompletionContent; +import ee.carlrobert.llm.client.google.completion.GoogleCompletionRequest; +import ee.carlrobert.llm.client.google.completion.GoogleContentPart; +import ee.carlrobert.llm.client.google.completion.GoogleContentPart.Blob; +import ee.carlrobert.llm.client.google.completion.GoogleGenerationConfig; +import ee.carlrobert.llm.client.google.models.GoogleModel; import ee.carlrobert.llm.client.llama.completion.LlamaCompletionRequest; import ee.carlrobert.llm.client.ollama.completion.request.OllamaChatCompletionMessage; import ee.carlrobert.llm.client.ollama.completion.request.OllamaChatCompletionRequest; @@ -221,6 +227,16 @@ public class CompletionRequestProvider { .setTemperature(configuration.getTemperature()).build(); } + public GoogleCompletionRequest buildGoogleChatCompletionRequest( + @Nullable String model, + CallParameters callParameters) { + var configuration = ConfigurationSettings.getCurrentState(); + return new GoogleCompletionRequest.Builder(buildGoogleMessages(model, callParameters)) + .generationConfig(new GoogleGenerationConfig.Builder() + .maxOutputTokens(configuration.getMaxTokens()) + .temperature(configuration.getTemperature()).build()).build(); + } + public Request buildCustomOpenAIChatCompletionRequest( CustomServiceChatCompletionSettingsState settings, CallParameters callParameters) { @@ -448,6 +464,83 @@ public class CompletionRequestProvider { return tryReducingMessagesOrThrow(messages, totalUsage, modelMaxTokens); } + private List buildGoogleMessages(CallParameters callParameters) { + var message = callParameters.getMessage(); + var messages = new ArrayList(); + // Gemini API does not support direct 'system' prompts: + // see https://www.reddit.com/r/Bard/comments/1b90i8o/does_gemini_have_a_system_prompt_option_while/ + if (callParameters.getConversationType() == ConversationType.DEFAULT) { + String systemPrompt = ConfigurationSettings.getCurrentState().getSystemPrompt(); + messages.add(new GoogleCompletionContent("user", List.of(systemPrompt))); + messages.add(new GoogleCompletionContent("model", List.of("Understood."))); + } + if (callParameters.getConversationType() == ConversationType.FIX_COMPILE_ERRORS) { + messages.add( + new GoogleCompletionContent("user", List.of(FIX_COMPILE_ERRORS_SYSTEM_PROMPT))); + messages.add(new GoogleCompletionContent("model", List.of("Understood."))); + } + + for (var prevMessage : conversation.getMessages()) { + if (callParameters.isRetry() && prevMessage.getId().equals(message.getId())) { + break; + } + var prevMessageImageFilePath = prevMessage.getImageFilePath(); + if (prevMessageImageFilePath != null && !prevMessageImageFilePath.isEmpty()) { + try { + var imageFilePath = Path.of(prevMessageImageFilePath); + var imageData = Files.readAllBytes(imageFilePath); + var imageMediaType = FileUtil.getImageMediaType(imageFilePath.getFileName().toString()); + messages.add(new GoogleCompletionContent( + List.of( + new GoogleContentPart(null, new Blob(imageMediaType, imageData)), + new GoogleContentPart(prevMessage.getPrompt())), "user")); + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + messages.add(new GoogleCompletionContent("user", List.of(prevMessage.getPrompt()))); + } + messages.add(new GoogleCompletionContent("model", List.of(prevMessage.getResponse()))); + } + + if (callParameters.getImageMediaType() != null && callParameters.getImageData().length > 0) { + messages.add(new GoogleCompletionContent( + List.of( + new GoogleContentPart(null, + new Blob(callParameters.getImageMediaType(), callParameters.getImageData())), + new GoogleContentPart(message.getPrompt())), "user")); + } else { + messages.add(new GoogleCompletionContent("user", List.of(message.getPrompt()))); + } + return messages; + } + + private List buildGoogleMessages( + @Nullable String model, + CallParameters callParameters) { + var messages = buildGoogleMessages(callParameters); + + if (model == null) { + return messages; + } + + int totalUsage = messages.parallelStream() + .mapToInt(message -> encodingManager.countMessageTokens(message.getRole(), + String.join(",", message.getParts().stream().map(GoogleContentPart::getText).toList()))) + .sum() + ConfigurationSettings.getCurrentState().getMaxTokens(); + int modelMaxTokens; + try { + modelMaxTokens = GoogleModel.findByCode(model).getMaxTokens(); + + if (totalUsage <= modelMaxTokens) { + return messages; + } + } catch (NoSuchElementException ex) { + return messages; + } + return tryReducingGoogleMessagesOrThrow(messages, totalUsage, modelMaxTokens); + } + private List tryReducingMessagesOrThrow( List messages, int totalUsage, @@ -473,4 +566,29 @@ public class CompletionRequestProvider { return messages.stream().filter(Objects::nonNull).toList(); } + + private List tryReducingGoogleMessagesOrThrow( + List messages, + int totalUsage, + int modelMaxTokens) { + if (!ConversationsState.getInstance().discardAllTokenLimits) { + if (!conversation.isDiscardTokenLimit()) { + throw new TotalUsageExceededException(); + } + } + + // skip the system prompt + for (int i = 1; i < messages.size(); i++) { + if (totalUsage <= modelMaxTokens) { + break; + } + + var message = messages.get(i); + totalUsage -= encodingManager.countMessageTokens(message.getRole(), + String.join(",", message.getParts().stream().map(GoogleContentPart::getText).toList())); + messages.set(i, null); + } + + return messages.stream().filter(Objects::nonNull).toList(); + } } diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java index 1f78000e..47e117a9 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java @@ -20,18 +20,23 @@ import ee.carlrobert.codegpt.settings.service.ServiceType; import ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettings; import ee.carlrobert.codegpt.settings.service.azure.AzureSettings; import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings; +import ee.carlrobert.codegpt.settings.service.google.GoogleSettings; +import ee.carlrobert.codegpt.settings.service.google.GoogleSettingsState; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; import ee.carlrobert.llm.client.DeserializationUtil; import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionRequest; import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionStandardMessage; +import ee.carlrobert.llm.client.google.completion.GoogleCompletionContent; +import ee.carlrobert.llm.client.google.completion.GoogleCompletionRequest; +import ee.carlrobert.llm.client.google.completion.GoogleGenerationConfig; import ee.carlrobert.llm.client.llama.completion.LlamaCompletionRequest; import ee.carlrobert.llm.client.ollama.completion.request.OllamaChatCompletionMessage; import ee.carlrobert.llm.client.ollama.completion.request.OllamaChatCompletionRequest; 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.OpenAIChatCompletionRequest.Builder; import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionStandardMessage; import ee.carlrobert.llm.client.openai.completion.response.OpenAIChatCompletionResponse; import ee.carlrobert.llm.client.openai.completion.response.OpenAIChatCompletionResponseChoice; @@ -110,6 +115,16 @@ public final class CompletionRequestService { case OLLAMA -> CompletionClientProvider.getOllamaClient().getChatCompletionAsync( requestProvider.buildOllamaChatCompletionRequest(callParameters), eventListener); + case GOOGLE -> { + var settings = ApplicationManager.getApplication() + .getService(GoogleSettings.class).getState(); + yield CompletionClientProvider.getGoogleClient().getChatCompletionAsync( + requestProvider.buildGoogleChatCompletionRequest( + settings.getModel(), + callParameters), + settings.getModel(), + eventListener); + } }; } @@ -142,7 +157,7 @@ public final class CompletionRequestService { String gitDiff, CompletionEventListener eventListener) { var configuration = ConfigurationSettings.getCurrentState(); - var openaiRequest = new OpenAIChatCompletionRequest.Builder(List.of( + var openaiRequest = new Builder(List.of( new OpenAIChatCompletionStandardMessage("system", systemPrompt), new OpenAIChatCompletionStandardMessage("user", gitDiff))) .setModel(OpenAISettings.getCurrentState().getModel()) @@ -212,6 +227,21 @@ public final class CompletionRequestService { ).build(); CompletionClientProvider.getOllamaClient().getChatCompletionAsync(request, eventListener); break; + case GOOGLE: + GoogleSettingsState state = ApplicationManager.getApplication() + .getService(GoogleSettings.class).getState(); + CompletionClientProvider.getGoogleClient() + .getChatCompletionAsync(new GoogleCompletionRequest.Builder( + List.of( + new GoogleCompletionContent("user", List.of(systemPrompt)), + new GoogleCompletionContent("model", List.of("Understood.")), + new GoogleCompletionContent("user", List.of(gitDiff)) + )) + .generationConfig(new GoogleGenerationConfig.Builder() + .maxOutputTokens(configuration.getMaxTokens()) + .temperature(configuration.getTemperature()).build()) + .build(), state.getModel(), eventListener); + break; default: LOG.debug("Unknown service: {}", selectedService); break; @@ -255,6 +285,7 @@ public final class CompletionRequestService { : CredentialKey.AZURE_ACTIVE_DIRECTORY_TOKEN); case CUSTOM_OPENAI, ANTHROPIC, LLAMA_CPP, OLLAMA -> true; case YOU -> false; + case GOOGLE -> CredentialsStore.INSTANCE.isCredentialSet(CredentialKey.GOOGLE_API_KEY); }; } diff --git a/src/main/java/ee/carlrobert/codegpt/conversations/ConversationService.java b/src/main/java/ee/carlrobert/codegpt/conversations/ConversationService.java index fcded8e6..d11a29e3 100644 --- a/src/main/java/ee/carlrobert/codegpt/conversations/ConversationService.java +++ b/src/main/java/ee/carlrobert/codegpt/conversations/ConversationService.java @@ -8,6 +8,7 @@ import ee.carlrobert.codegpt.settings.GeneralSettings; import ee.carlrobert.codegpt.settings.service.ServiceType; import ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettings; import ee.carlrobert.codegpt.settings.service.azure.AzureSettings; +import ee.carlrobert.codegpt.settings.service.google.GoogleSettings; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; @@ -203,6 +204,11 @@ public final class ConversationService { .getService(OllamaSettings.class) .getState() .getModel(); + case GOOGLE -> + ApplicationManager.getApplication() + .getService(GoogleSettings.class) + .getState() + .getModel(); }; } } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettings.java b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettings.java index 423fcc06..b72ee03d 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettings.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettings.java @@ -10,6 +10,7 @@ import ee.carlrobert.codegpt.conversations.Conversation; import ee.carlrobert.codegpt.settings.service.ServiceType; import ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettings; import ee.carlrobert.codegpt.settings.service.azure.AzureSettings; +import ee.carlrobert.codegpt.settings.service.google.GoogleSettings; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; @@ -107,6 +108,11 @@ public class GeneralSettings implements PersistentStateComponent(); serviceComboBoxModel.addAll(Arrays.stream(ServiceType.values()).toList()); serviceComboBox = new ComboBox<>(serviceComboBoxModel); @@ -121,6 +125,10 @@ public class GeneralSettingsComponent { return ollamaSettingsForm; } + public GoogleSettingsForm getGoogleSettingsForm() { + return googleSettingsForm; + } + public ServiceType getSelectedService() { return serviceComboBox.getItem(); } @@ -153,6 +161,7 @@ public class GeneralSettingsComponent { youSettingsForm.resetForm(); llamaSettingsForm.resetForm(); ollamaSettingsForm.resetForm(); + googleSettingsForm.resetForm(); } static class DynamicCardLayout extends CardLayout { diff --git a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java index 17a5fe8e..cef436f8 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java @@ -4,6 +4,7 @@ import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.A import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.AZURE_ACTIVE_DIRECTORY_TOKEN; import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.AZURE_OPENAI_API_KEY; import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CUSTOM_SERVICE_API_KEY; +import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.GOOGLE_API_KEY; import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.LLAMA_API_KEY; import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.OPENAI_API_KEY; @@ -18,10 +19,9 @@ 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.google.GoogleSettingsForm; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.llama.form.LlamaSettingsForm; -import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings; -import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettingsForm; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettingsForm; import ee.carlrobert.codegpt.settings.service.you.YouSettings; @@ -71,7 +71,8 @@ public class GeneralSettingsConfigurable implements Configurable { || AzureSettings.getInstance().isModified(component.getAzureSettingsForm()) || YouSettings.getInstance().isModified(component.getYouSettingsForm()) || LlamaSettings.getInstance().isModified(component.getLlamaSettingsForm()) - || component.getOllamaSettingsForm().isModified(); + || component.getOllamaSettingsForm().isModified() + || component.getGoogleSettingsForm().isModified(); } @Override @@ -88,6 +89,7 @@ public class GeneralSettingsConfigurable implements Configurable { applyYouSettings(component.getYouSettingsForm()); applyLlamaSettings(component.getLlamaSettingsForm()); component.getOllamaSettingsForm().applyChanges(); + applyGoogleSettings(component.getGoogleSettingsForm()); var serviceChanged = component.getSelectedService() != settings.getSelectedService(); var modelChanged = !OpenAISettings.getCurrentState().getModel() @@ -137,8 +139,9 @@ public class GeneralSettingsConfigurable implements Configurable { form.getActiveDirectoryToken()); } - private void applyOllamaSettings(OllamaSettingsForm form) { + private void applyGoogleSettings(GoogleSettingsForm form) { form.applyChanges(); + CredentialsStore.INSTANCE.setCredential(GOOGLE_API_KEY, form.getApiKey()); } @Override 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 d8c6425b..dc749997 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceType.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceType.java @@ -9,7 +9,8 @@ public enum ServiceType { 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"), - OLLAMA("OLLAMA", "service.ollama.title", "ollama.chat.completion"); + OLLAMA("OLLAMA", "service.ollama.title", "ollama.chat.completion"), + GOOGLE("GOOGLE", "service.google.title", "google.chat.completion"); private final String code; private final String label; 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 38457966..5a22d28d 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 @@ -113,6 +113,12 @@ public class ModelComboBoxAction extends ComboBoxAction { actionGroup.addSeparator("Ollama"); ollamaSettings.getAvailableModels().forEach(model -> actionGroup.add(createOllamaModelAction(model, presentation))); + actionGroup.addSeparator(); + actionGroup.add(createModelAction( + ServiceType.GOOGLE, + "Google (Gemini)", + Icons.Google, + presentation)); if (YouUserManager.getInstance().isSubscribed()) { actionGroup.addSeparator("You.com"); @@ -193,6 +199,10 @@ public class ModelComboBoxAction extends ComboBoxAction { templatePresentation.setIcon(Icons.Ollama); templatePresentation.setText(ollamaSettings.getModel()); break; + case GOOGLE: + templatePresentation.setText("Google (Gemini)"); + templatePresentation.setIcon(Icons.Google); + break; default: break; } diff --git a/src/main/java/ee/carlrobert/codegpt/ui/ModelIconLabel.java b/src/main/java/ee/carlrobert/codegpt/ui/ModelIconLabel.java index 02fdd5a7..f125a5b1 100644 --- a/src/main/java/ee/carlrobert/codegpt/ui/ModelIconLabel.java +++ b/src/main/java/ee/carlrobert/codegpt/ui/ModelIconLabel.java @@ -27,6 +27,9 @@ public class ModelIconLabel extends JBLabel { if ("llama.chat.completion".equals(clientCode)) { setIcon(Icons.Llama); } + if ("google.chat.completion".equals(clientCode)) { + setIcon(Icons.Google); + } setText(formatModelName(modelCode)); setFont(JBFont.small()); setHorizontalAlignment(SwingConstants.LEADING); diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt index 9faf9666..121c4a54 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt @@ -32,6 +32,7 @@ abstract class CodeCompletionFeatureToggleActions( ANTHROPIC, AZURE, YOU, + GOOGLE, null -> { /* no-op for these services */ } } @@ -50,6 +51,7 @@ abstract class CodeCompletionFeatureToggleActions( ANTHROPIC, AZURE, YOU, + GOOGLE, null -> false } } @@ -66,6 +68,7 @@ abstract class CodeCompletionFeatureToggleActions( OLLAMA -> service().state.codeCompletionsEnabled ANTHROPIC, AZURE, + GOOGLE, YOU -> false } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt index 27f3d43e..9e632510 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt @@ -75,6 +75,7 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { ServiceType.ANTHROPIC, ServiceType.AZURE, ServiceType.YOU, + ServiceType.GOOGLE, null -> false } return event is InlineCompletionEvent.DocumentChange && codeCompletionsEnabled diff --git a/src/main/kotlin/ee/carlrobert/codegpt/credentials/CredentialsStore.kt b/src/main/kotlin/ee/carlrobert/codegpt/credentials/CredentialsStore.kt index 284c6084..de6f5055 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/credentials/CredentialsStore.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/credentials/CredentialsStore.kt @@ -40,6 +40,7 @@ object CredentialsStore { AZURE_OPENAI_API_KEY, AZURE_ACTIVE_DIRECTORY_TOKEN, YOU_ACCOUNT_PASSWORD, - LLAMA_API_KEY + LLAMA_API_KEY, + GOOGLE_API_KEY } } \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/google/GoogleSettings.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/google/GoogleSettings.kt new file mode 100644 index 00000000..5ca647a9 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/google/GoogleSettings.kt @@ -0,0 +1,11 @@ +package ee.carlrobert.codegpt.settings.service.google + +import com.intellij.openapi.components.* +import ee.carlrobert.llm.client.google.models.GoogleModel + +@State(name = "CodeGPT_GoogleSettings_210", storages = [Storage("CodeGPT_GoogleSettings_210.xml")]) +class GoogleSettings : SimplePersistentStateComponent(GoogleSettingsState()) + +class GoogleSettingsState : BaseState() { + var model by string(GoogleModel.GEMINI_PRO.code) +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/google/GoogleSettingsForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/google/GoogleSettingsForm.kt new file mode 100644 index 00000000..3491a4df --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/google/GoogleSettingsForm.kt @@ -0,0 +1,92 @@ +package ee.carlrobert.codegpt.settings.service.google + +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.JBPasswordField +import com.intellij.util.ui.FormBuilder +import com.intellij.util.ui.UI +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 ee.carlrobert.llm.client.google.models.GoogleModel +import javax.swing.JPanel +import javax.swing.event.HyperlinkEvent + +class GoogleSettingsForm { + private val apiKeyField = JBPasswordField() + private val completionModelComboBox: ComboBox + + init { + val state = service().state + apiKeyField.columns = 30 + apiKeyField.text = + getCredential(CredentialKey.GOOGLE_API_KEY) + completionModelComboBox = ComboBox( + EnumComboBoxModel(GoogleModel::class.java) + ) + completionModelComboBox.selectedItem = GoogleModel.findByCode(state.model) + } + + fun getForm(): JPanel = FormBuilder.createFormBuilder() + .addComponent(TitledSeparator(CodeGPTBundle.get("shared.configuration"))) + .addComponent( + UIUtil.withEmptyLeftBorder( + UI.PanelFactory.grid() + .add( + UI.PanelFactory.panel(apiKeyField) + .withLabel(CodeGPTBundle.get("settingsConfigurable.shared.apiKey.label")) + .resizeX(false) + .withComment(CodeGPTBundle.get("settingsConfigurable.service.google.apiKey.comment")) + .withCommentHyperlinkListener { event: HyperlinkEvent? -> + UIUtil.handleHyperlinkClicked( + event + ) + }) + + .add( + UI.PanelFactory.panel(completionModelComboBox) + .withLabel(CodeGPTBundle.get("settingsConfigurable.shared.model.label")) + .resizeX(false) + .withComment(CodeGPTBundle.get("settingsConfigurable.service.google.model.comment")) + .withCommentHyperlinkListener { event: HyperlinkEvent? -> + UIUtil.handleHyperlinkClicked( + event + ) + } + ) + .createPanel() + ) + ) + .addComponentFillVertically(JPanel(), 0) + .panel + + + fun getApiKey(): String? = String(apiKeyField.password).ifEmpty { null } + + fun getModel(): String = (completionModelComboBox.model + .selectedItem as GoogleModel) + .code + + fun getCurrentState() = GoogleSettingsState().apply { model = getModel() } + + fun resetForm() { + val state = service().state + apiKeyField.text = + getCredential(CredentialKey.GOOGLE_API_KEY) + completionModelComboBox.selectedItem = GoogleModel.findByCode(state.model) + } + + fun isModified(): Boolean = service().state.run { + model != getModel() || getApiKey() != getCredential(CredentialKey.GOOGLE_API_KEY) + } + + fun applyChanges() { + service().state.run { + model = getModel() + } + } + +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 2c883574..bf0503bb 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -37,6 +37,7 @@ + diff --git a/src/main/resources/icons/google.svg b/src/main/resources/icons/google.svg new file mode 100644 index 00000000..bc301fcd --- /dev/null +++ b/src/main/resources/icons/google.svg @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index b4ae54fd..04b1d7e5 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -23,6 +23,8 @@ settingsConfigurable.service.openai.apiKey.comment=You can find the API key in y settingsConfigurable.service.openai.customModel.label=Custom model: settingsConfigurable.service.openai.organization.label=Organization: settingsConfigurable.section.openai.organization.comment=Useful when you are part of multiple organizations optional +settingsConfigurable.service.google.apiKey.comment=You can find the API key in your User settings. +settingsConfigurable.service.google.model.comment=Note: Gemini Vision models do not yet support chats. settingsConfigurable.service.anthropic.apiKey.comment=You can find the API key in your User settings. settingsConfigurable.service.anthropic.apiVersion.comment=We always recommend using the latest API version whenever possible. settingsConfigurable.service.anthropic.model.comment=For details on model comparison metrics, see model comparison. @@ -175,6 +177,7 @@ service.azure.title=Azure Service service.you.title=You.com Service (Free, Cloud) service.llama.title=LLaMA C/C++ Port (Free, Local) service.ollama.title=Ollama (Free, Local) +service.google.title=Google Service validation.error.fieldRequired=This field is required. validation.error.invalidEmail=The email you entered is invalid. validation.error.mustBeNumber=Value must be number. diff --git a/src/test/kotlin/ee/carlrobert/codegpt/completions/DefaultCompletionRequestHandlerTest.kt b/src/test/kotlin/ee/carlrobert/codegpt/completions/DefaultCompletionRequestHandlerTest.kt index 86161a1e..9cbbdea7 100644 --- a/src/test/kotlin/ee/carlrobert/codegpt/completions/DefaultCompletionRequestHandlerTest.kt +++ b/src/test/kotlin/ee/carlrobert/codegpt/completions/DefaultCompletionRequestHandlerTest.kt @@ -7,10 +7,7 @@ import ee.carlrobert.codegpt.conversations.message.Message import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings import ee.carlrobert.llm.client.http.RequestEntity import ee.carlrobert.llm.client.http.exchange.StreamHttpExchange -import ee.carlrobert.llm.client.util.JSONUtil.e -import ee.carlrobert.llm.client.util.JSONUtil.jsonArray -import ee.carlrobert.llm.client.util.JSONUtil.jsonMap -import ee.carlrobert.llm.client.util.JSONUtil.jsonMapResponse +import ee.carlrobert.llm.client.util.JSONUtil.* import org.apache.http.HttpHeaders import org.assertj.core.api.Assertions.assertThat import testsupport.IntegrationTest @@ -171,6 +168,43 @@ class DefaultCompletionRequestHandlerTest : IntegrationTest() { waitExpecting { "Hello!" == message.response } } + + fun testGoogleChatCompletionCall() { + useGoogleService() + ConfigurationSettings.getCurrentState().systemPrompt = "TEST_SYSTEM_PROMPT" + val message = Message("TEST_PROMPT") + val conversation = ConversationService.getInstance().startConversation() + val requestHandler = CompletionRequestHandler(getRequestEventListener(message)) + expectGoogle(StreamHttpExchange { request: RequestEntity -> + assertThat(request.uri.path).isEqualTo("/v1/models/gemini-pro:streamGenerateContent") + assertThat(request.method).isEqualTo("POST") + assertThat(request.uri.query).isEqualTo("key=TEST_API_KEY&alt=sse") + assertThat(request.body) + .extracting("contents") + .isEqualTo( + listOf( + mapOf("parts" to listOf(mapOf("text" to "TEST_SYSTEM_PROMPT")), "role" to "user"), + mapOf("parts" to listOf(mapOf("text" to "Understood.")), "role" to "model"), + mapOf("parts" to listOf(mapOf("text" to "TEST_PROMPT")), "role" to "user"), + ) + ) + listOf( + jsonMapResponse( + "candidates", + jsonArray(jsonMap("content", jsonMap("parts", jsonArray(jsonMap("text", "Hello"))))) + ), + jsonMapResponse( + "candidates", + jsonArray(jsonMap("content", jsonMap("parts", jsonArray(jsonMap("text", "!"))))) + ) + ) + }) + + requestHandler.call(CallParameters(conversation, ConversationType.DEFAULT, message, false)) + + waitExpecting { "Hello!" == message.response } + } + private fun getRequestEventListener(message: Message): CompletionResponseEventListener { return object : CompletionResponseEventListener { override fun handleCompleted(fullMessage: String, callParameters: CallParameters) { diff --git a/src/test/kotlin/testsupport/mixin/ShortcutsTestMixin.kt b/src/test/kotlin/testsupport/mixin/ShortcutsTestMixin.kt index 284a7ba9..2ce62938 100644 --- a/src/test/kotlin/testsupport/mixin/ShortcutsTestMixin.kt +++ b/src/test/kotlin/testsupport/mixin/ShortcutsTestMixin.kt @@ -1,14 +1,16 @@ package testsupport.mixin +import com.intellij.openapi.components.service import com.intellij.testFramework.PlatformTestUtil -import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.AZURE_OPENAI_API_KEY -import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.OPENAI_API_KEY +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.* import ee.carlrobert.codegpt.credentials.CredentialsStore.setCredential import ee.carlrobert.codegpt.settings.GeneralSettings import ee.carlrobert.codegpt.settings.service.ServiceType import ee.carlrobert.codegpt.settings.service.azure.AzureSettings +import ee.carlrobert.codegpt.settings.service.google.GoogleSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings +import ee.carlrobert.llm.client.google.models.GoogleModel import java.util.function.BooleanSupplier interface ShortcutsTestMixin { @@ -41,6 +43,13 @@ interface ShortcutsTestMixin { LlamaSettings.getCurrentState().serverPort = null } + fun useGoogleService() { + GeneralSettings.getCurrentState().selectedService = ServiceType.GOOGLE + setCredential(GOOGLE_API_KEY, "TEST_API_KEY") + service().state.model = GoogleModel.GEMINI_PRO.code + } + + fun waitExpecting(condition: BooleanSupplier?) { PlatformTestUtil.waitWithEventsDispatching( "Waiting for message response timed out or did not meet expected conditions",