diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1b82b881..31b8fa19 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ jsoup = "1.19.1" jtokkit = "1.1.0" junit = "5.12.1" kotlin = "2.1.20" -llm-client = "0.8.44" +llm-client = "0.8.46" okio = "3.10.2" tree-sitter = "0.24.5" grpc = "1.71.0" diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java index 3daebc43..696ba99c 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.llm.client.anthropic.ClaudeClient; import ee.carlrobert.llm.client.codegpt.CodeGPTClient; import ee.carlrobert.llm.client.google.GoogleClient; import ee.carlrobert.llm.client.llama.LlamaClient; +import ee.carlrobert.llm.client.mistral.MistralClient; import ee.carlrobert.llm.client.ollama.OllamaClient; import ee.carlrobert.llm.client.openai.OpenAIClient; import java.net.InetSocketAddress; @@ -73,6 +74,10 @@ public class CompletionClientProvider { .build(getDefaultClientBuilder()); } + public static MistralClient getMistralClient() { + return new MistralClient(getCredential(CredentialKey.MistralApiKey.INSTANCE), getDefaultClientBuilder()); + } + public static OkHttpClient.Builder getDefaultClientBuilder() { OkHttpClient.Builder builder = new OkHttpClient.Builder(); CertificateManager certificateManager = CertificateManager.getInstance(); diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java index 8a7683f5..26af6898 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java @@ -122,6 +122,8 @@ public final class CompletionRequestService { .getChatCompletionAsync(completionRequest, eventListener); case OLLAMA -> CompletionClientProvider.getOllamaClient() .getChatCompletionAsync(completionRequest, eventListener); + case MISTRAL -> CompletionClientProvider.getMistralClient() + .getChatCompletionAsync(completionRequest, eventListener); default -> throw new RuntimeException("Unknown service selected"); }; } @@ -152,17 +154,16 @@ public final class CompletionRequestService { throw new IllegalStateException("Unknown request type: " + request.getClass()); } - public String getChatCompletion(CompletionRequest request, ServiceType serviceType) { - return getChatCompletion(request, serviceType, FeatureType.CHAT); - } - - public String getChatCompletion(CompletionRequest request, ServiceType serviceType, FeatureType featureType) { + public String getChatCompletion(CompletionRequest request, ServiceType serviceType, + FeatureType featureType) { if (request instanceof OpenAIChatCompletionRequest completionRequest) { var response = switch (serviceType) { case OPENAI -> CompletionClientProvider.getOpenAIClient() .getChatCompletion(completionRequest); case OLLAMA -> CompletionClientProvider.getOllamaClient() .getChatCompletion(completionRequest); + case MISTRAL -> CompletionClientProvider.getMistralClient() + .getChatCompletion(completionRequest); default -> throw new RuntimeException("Unknown service selected"); }; return tryExtractContent(response).orElseThrow(); @@ -227,6 +228,8 @@ public final class CompletionRequestService { CredentialKey.AnthropicApiKey.INSTANCE ); case GOOGLE -> CredentialsStore.INSTANCE.isCredentialSet(CredentialKey.GoogleApiKey.INSTANCE); + case MISTRAL -> + CredentialsStore.INSTANCE.isCredentialSet(CredentialKey.MistralApiKey.INSTANCE); case PROXYAI, CUSTOM_OPENAI, LLAMA_CPP, OLLAMA -> true; }; } 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 2fb4b3eb..29bc8319 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceType.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceType.java @@ -10,6 +10,7 @@ public enum ServiceType { CUSTOM_OPENAI("CUSTOM_OPENAI", "service.custom.openai.title", "custom.openai.chat.completion"), ANTHROPIC("ANTHROPIC", "service.anthropic.title", "anthropic.chat.completion"), GOOGLE("GOOGLE", "service.google.title", "google.chat.completion"), + MISTRAL("MISTRAL", "service.mistral.title", "mistral.chat.completion"), LLAMA_CPP("LLAMA_CPP", "service.llama.title", "llama.chat.completion"), OLLAMA("OLLAMA", "service.ollama.title", "ollama.chat.completion"); diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/mistral/MistralSettings.java b/src/main/java/ee/carlrobert/codegpt/settings/service/mistral/MistralSettings.java new file mode 100644 index 00000000..4b6f9e37 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/mistral/MistralSettings.java @@ -0,0 +1,32 @@ +package ee.carlrobert.codegpt.settings.service.mistral; + +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 org.jetbrains.annotations.NotNull; + +@State(name = "CodeGPT_MistralSettings", storages = @Storage("CodeGPT_MistralSettings.xml")) +public class MistralSettings implements PersistentStateComponent { + + private MistralSettingsState state = new MistralSettingsState(); + + @Override + @NotNull + public MistralSettingsState getState() { + return state; + } + + @Override + public void loadState(@NotNull MistralSettingsState state) { + this.state = state; + } + + public static MistralSettingsState getCurrentState() { + return getInstance().getState(); + } + + public static MistralSettings getInstance() { + return ApplicationManager.getApplication().getService(MistralSettings.class); + } +} \ No newline at end of file diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/mistral/MistralSettingsForm.java b/src/main/java/ee/carlrobert/codegpt/settings/service/mistral/MistralSettingsForm.java new file mode 100644 index 00000000..f71f5054 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/mistral/MistralSettingsForm.java @@ -0,0 +1,59 @@ +package ee.carlrobert.codegpt.settings.service.mistral; + +import com.intellij.openapi.application.ApplicationManager; +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; +import ee.carlrobert.codegpt.ui.UIUtil; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import org.jetbrains.annotations.Nullable; + +public class MistralSettingsForm { + + private final JBPasswordField apiKeyField; + + public MistralSettingsForm(MistralSettingsState settings) { + apiKeyField = new JBPasswordField(); + apiKeyField.setColumns(30); + ApplicationManager.getApplication().executeOnPooledThread(() -> { + var apiKey = CredentialsStore.getCredential( + CredentialsStore.CredentialKey.MistralApiKey.INSTANCE + ); + SwingUtilities.invokeLater(() -> apiKeyField.setText(apiKey)); + }); + } + + public JPanel getForm() { + return FormBuilder.createFormBuilder() + .addComponent(UI.PanelFactory.grid() + .add(UI.PanelFactory.panel(apiKeyField) + .withLabel(CodeGPTBundle.get("settingsConfigurable.shared.apiKey.label")) + .resizeX(false) + .withComment( + "You can find the API key in your Mistral Console.") + .withCommentHyperlinkListener(UIUtil::handleHyperlinkClicked)) + .createPanel()) + .addComponentFillVertically(new JPanel(), 0) + .getPanel(); + } + + public MistralSettingsState getCurrentState() { + var state = new MistralSettingsState(); + return state; + } + + public void resetForm() { + var state = MistralSettings.getCurrentState(); + apiKeyField.setText( + CredentialsStore.getCredential(CredentialsStore.CredentialKey.MistralApiKey.INSTANCE) + ); + } + + public @Nullable String getApiKey() { + var apiKey = new String(apiKeyField.getPassword()); + return apiKey.isEmpty() ? null : apiKey; + } +} \ No newline at end of file diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/mistral/MistralSettingsState.java b/src/main/java/ee/carlrobert/codegpt/settings/service/mistral/MistralSettingsState.java new file mode 100644 index 00000000..e64047e9 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/mistral/MistralSettingsState.java @@ -0,0 +1,33 @@ +package ee.carlrobert.codegpt.settings.service.mistral; + +import java.util.Objects; + +public class MistralSettingsState { + + private boolean codeCompletionsEnabled = true; + + public boolean isCodeCompletionsEnabled() { + return codeCompletionsEnabled; + } + + public void setCodeCompletionsEnabled(boolean codeCompletionsEnabled) { + this.codeCompletionsEnabled = codeCompletionsEnabled; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MistralSettingsState that = (MistralSettingsState) o; + return codeCompletionsEnabled == that.codeCompletionsEnabled; + } + + @Override + public int hashCode() { + return Objects.hash(codeCompletionsEnabled); + } +} \ No newline at end of file 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 68b7cf8e..5917b7fb 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 @@ -5,6 +5,7 @@ import static ee.carlrobert.codegpt.settings.service.ServiceType.ANTHROPIC; import static ee.carlrobert.codegpt.settings.service.ServiceType.CUSTOM_OPENAI; import static ee.carlrobert.codegpt.settings.service.ServiceType.GOOGLE; import static ee.carlrobert.codegpt.settings.service.ServiceType.LLAMA_CPP; +import static ee.carlrobert.codegpt.settings.service.ServiceType.MISTRAL; import static ee.carlrobert.codegpt.settings.service.ServiceType.OLLAMA; import static ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI; import static ee.carlrobert.codegpt.settings.service.ServiceType.PROXYAI; @@ -199,7 +200,7 @@ public class ModelComboBoxAction extends ComboBoxAction { List.of( GoogleModel.GEMINI_2_5_PRO_PREVIEW, GoogleModel.GEMINI_2_5_FLASH_PREVIEW, - GoogleModel.GEMINI_2_5_PRO_EXP, + GoogleModel.GEMINI_2_5_PRO, GoogleModel.GEMINI_2_0_PRO_EXP, GoogleModel.GEMINI_2_0_FLASH_THINKING_EXP, GoogleModel.GEMINI_2_0_FLASH, @@ -208,6 +209,17 @@ public class ModelComboBoxAction extends ComboBoxAction { actionGroup.add(googleGroup); } + if (availableProviders.contains(MISTRAL)) { + var mistralGroup = DefaultActionGroup.createPopupGroup(() -> "Mistral"); + mistralGroup.getTemplatePresentation().setIcon(Icons.Mistral); + List.of( + ModelRegistry.DEVSTRAL_MEDIUM_2507, + ModelRegistry.MISTRAL_LARGE_2411, + ModelRegistry.CODESTRAL_LATEST) + .forEach(model -> mistralGroup.add(createMistralModelAction(model, presentation))); + actionGroup.add(mistralGroup); + } + if (availableProviders.contains(LLAMA_CPP) || availableProviders.contains(OLLAMA)) { actionGroup.addSeparator("Offline"); @@ -302,6 +314,10 @@ public class ModelComboBoxAction extends ComboBoxAction { templatePresentation.setText(getGooglePresentationText()); templatePresentation.setIcon(Icons.Google); break; + case MISTRAL: + templatePresentation.setText(getMistralPresentationText()); + templatePresentation.setIcon(Icons.Mistral); + break; default: break; } @@ -458,4 +474,22 @@ public class ModelComboBoxAction extends ComboBoxAction { .setModel(FeatureType.CHAT, LlamaSettings.getCurrentState().getHuggingFaceModel().getCode(), LLAMA_CPP)); } + + private AnAction createMistralModelAction(String modelCode, Presentation comboBoxPresentation) { + var modelName = ModelRegistry.getInstance().getModelDisplayName(MISTRAL, modelCode); + return createModelAction( + MISTRAL, + modelName, + Icons.Mistral, + comboBoxPresentation, + () -> ApplicationManager.getApplication().getService(ModelSettings.class) + .setModel(FeatureType.CHAT, modelCode, MISTRAL)); + } + + private String getMistralPresentationText() { + var chatModel = ApplicationManager.getApplication().getService(ModelSettings.class).getState() + .getModelSelection(FeatureType.CHAT); + var modelCode = chatModel != null ? chatModel.getModel() : null; + return ModelRegistry.getInstance().getModelDisplayName(MISTRAL, modelCode); + } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt index cad4c8a5..99031533 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt @@ -45,7 +45,8 @@ abstract class CodeCompletionFeatureToggleActions( } ANTHROPIC, - GOOGLE -> { + GOOGLE, + MISTRAL -> { } } } @@ -61,7 +62,8 @@ abstract class CodeCompletionFeatureToggleActions( OPENAI, CUSTOM_OPENAI, LLAMA_CPP, - OLLAMA -> true + OLLAMA, + MISTRAL -> true ANTHROPIC, GOOGLE -> false diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt index 46f070c5..7f546d5b 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt @@ -7,12 +7,12 @@ import ee.carlrobert.codegpt.completions.llama.LlamaModel import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential import ee.carlrobert.codegpt.settings.Placeholder.* +import ee.carlrobert.codegpt.settings.service.FeatureType +import ee.carlrobert.codegpt.settings.service.ModelSelectionService import ee.carlrobert.codegpt.settings.service.custom.CustomServicesSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettingsState import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings -import ee.carlrobert.codegpt.settings.service.ModelSelectionService -import ee.carlrobert.codegpt.settings.service.FeatureType import ee.carlrobert.llm.client.llama.completion.LlamaCompletionRequest import ee.carlrobert.llm.client.ollama.completion.request.OllamaCompletionRequest import ee.carlrobert.llm.client.ollama.completion.request.OllamaParameters @@ -40,6 +40,9 @@ object CodeCompletionRequestFactory { @JvmStatic fun buildOpenAIRequest(details: InfillRequest): OpenAITextCompletionRequest { return OpenAITextCompletionRequest.Builder(details.prefix) + .setModel( + ModelSelectionService.getInstance().getModelForFeature(FeatureType.CODE_COMPLETION) + ) .setSuffix(details.suffix) .setStream(true) .setMaxTokens(MAX_TOKENS) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionService.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionService.kt index b733fb56..2ed7e29c 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionService.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionService.kt @@ -8,13 +8,14 @@ import ee.carlrobert.codegpt.codecompletions.CodeCompletionRequestFactory.buildL import ee.carlrobert.codegpt.codecompletions.CodeCompletionRequestFactory.buildOllamaRequest import ee.carlrobert.codegpt.codecompletions.CodeCompletionRequestFactory.buildOpenAIRequest import ee.carlrobert.codegpt.completions.CompletionClientProvider -import ee.carlrobert.codegpt.settings.service.ServiceType -import ee.carlrobert.codegpt.settings.service.ServiceType.* import ee.carlrobert.codegpt.settings.service.FeatureType import ee.carlrobert.codegpt.settings.service.ModelSelectionService +import ee.carlrobert.codegpt.settings.service.ServiceType +import ee.carlrobert.codegpt.settings.service.ServiceType.* import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings import ee.carlrobert.codegpt.settings.service.custom.CustomServicesSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings +import ee.carlrobert.codegpt.settings.service.mistral.MistralSettings import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionEventSourceListener @@ -30,13 +31,16 @@ class CodeCompletionService(private val project: Project) { return ModelSelectionService.getInstance().getModelForFeature(FeatureType.CODE_COMPLETION) } - fun isCodeCompletionsEnabled(): Boolean = isCodeCompletionsEnabled(ModelSelectionService.getInstance().getServiceForFeature(FeatureType.CODE_COMPLETION)) + fun isCodeCompletionsEnabled(): Boolean = isCodeCompletionsEnabled( + ModelSelectionService.getInstance().getServiceForFeature(FeatureType.CODE_COMPLETION) + ) fun isCodeCompletionsEnabled(selectedService: ServiceType): Boolean = when (selectedService) { PROXYAI -> service().state.codeCompletionSettings.codeCompletionsEnabled OPENAI -> OpenAISettings.getCurrentState().isCodeCompletionsEnabled CUSTOM_OPENAI -> service().state.active.codeCompletionSettings.codeCompletionsEnabled + MISTRAL -> MistralSettings.getCurrentState().isCodeCompletionsEnabled LLAMA_CPP -> LlamaSettings.isCodeCompletionsPossible() OLLAMA -> service().state.codeCompletionsEnabled else -> false @@ -46,7 +50,8 @@ class CodeCompletionService(private val project: Project) { infillRequest: InfillRequest, eventListener: CompletionEventListener ): EventSource { - return when (val selectedService = ModelSelectionService.getInstance().getServiceForFeature(FeatureType.CODE_COMPLETION)) { + return when (val selectedService = + ModelSelectionService.getInstance().getServiceForFeature(FeatureType.CODE_COMPLETION)) { OPENAI -> CompletionClientProvider.getOpenAIClient() .getCompletionAsync(buildOpenAIRequest(infillRequest), eventListener) @@ -61,6 +66,9 @@ class CodeCompletionService(private val project: Project) { } ) + MISTRAL -> CompletionClientProvider.getMistralClient() + .getCodeCompletionAsync(buildOpenAIRequest(infillRequest), eventListener) + OLLAMA -> CompletionClientProvider.getOllamaClient() .getCompletionAsync(buildOllamaRequest(infillRequest), eventListener) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/DebouncedCodeCompletionProvider.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/DebouncedCodeCompletionProvider.kt index cdc09c86..46ec3027 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/DebouncedCodeCompletionProvider.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/DebouncedCodeCompletionProvider.kt @@ -112,6 +112,7 @@ class DebouncedCodeCompletionProvider : DebouncedInlineCompletionProvider() { ServiceType.CUSTOM_OPENAI -> service().state.active.codeCompletionSettings.codeCompletionsEnabled ServiceType.LLAMA_CPP -> LlamaSettings.isCodeCompletionsPossible() ServiceType.OLLAMA -> service().state.codeCompletionsEnabled + ServiceType.MISTRAL -> true // Mistral supports code completions ServiceType.ANTHROPIC, ServiceType.GOOGLE, null -> false diff --git a/src/main/kotlin/ee/carlrobert/codegpt/completions/CompletionRequestFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/completions/CompletionRequestFactory.kt index c0ae129e..bf3ec4b3 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/completions/CompletionRequestFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/completions/CompletionRequestFactory.kt @@ -29,6 +29,7 @@ interface CompletionRequestFactory { ServiceType.CUSTOM_OPENAI -> CustomOpenAIRequestFactory() ServiceType.ANTHROPIC -> ClaudeRequestFactory() ServiceType.GOOGLE -> GoogleRequestFactory() + ServiceType.MISTRAL -> MistralRequestFactory() ServiceType.OLLAMA -> OllamaRequestFactory() ServiceType.LLAMA_CPP -> LlamaRequestFactory() } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/completions/factory/MistralRequestFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/completions/factory/MistralRequestFactory.kt new file mode 100644 index 00000000..16097588 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/completions/factory/MistralRequestFactory.kt @@ -0,0 +1,50 @@ +package ee.carlrobert.codegpt.completions.factory + +import com.intellij.openapi.components.service +import ee.carlrobert.codegpt.completions.BaseRequestFactory +import ee.carlrobert.codegpt.completions.ChatCompletionParameters +import ee.carlrobert.codegpt.completions.factory.OpenAIRequestFactory.Companion.buildOpenAIMessages +import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings +import ee.carlrobert.codegpt.settings.service.FeatureType +import ee.carlrobert.codegpt.settings.service.ModelSelectionService +import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionRequest + +class MistralRequestFactory : BaseRequestFactory() { + + override fun createChatRequest(params: ChatCompletionParameters): OpenAIChatCompletionRequest { + val model = ModelSelectionService.getInstance().getModelForFeature(FeatureType.CHAT) + val configuration = service().state + + return OpenAIChatCompletionRequest.Builder( + buildOpenAIMessages( + model = model, + callParameters = params, + referencedFiles = params.referencedFiles, + conversationsHistory = params.history, + psiStructure = params.psiStructure, + ) + ) + .setModel(model) + .setMaxTokens(configuration.maxTokens) + .setMaxCompletionTokens(null) + .setStream(true) + .setTemperature(configuration.temperature.toDouble()) + .build() + } + + override fun createBasicCompletionRequest( + systemPrompt: String, + userPrompt: String, + maxTokens: Int, + stream: Boolean, + featureType: FeatureType + ): OpenAIChatCompletionRequest { + val model = ModelSelectionService.getInstance().getModelForFeature(featureType) + return OpenAIRequestFactory.createBasicCompletionRequest( + systemPrompt, + userPrompt, + model = model, + isStream = stream + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/credentials/CredentialsStore.kt b/src/main/kotlin/ee/carlrobert/codegpt/credentials/CredentialsStore.kt index 5d434dc7..adc3e907 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/credentials/CredentialsStore.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/credentials/CredentialsStore.kt @@ -68,5 +68,9 @@ object CredentialsStore { data object OllamaApikey : CredentialKey() { override val value: String = "OLLAMA_API_KEY" } + + data object MistralApiKey : CredentialKey() { + override val value: String = "MISTRAL_API_KEY" + } } } \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/migration/LegacySettingsMigration.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/migration/LegacySettingsMigration.kt index bcdf59b2..c78a28f5 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/migration/LegacySettingsMigration.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/migration/LegacySettingsMigration.kt @@ -9,7 +9,6 @@ import ee.carlrobert.codegpt.settings.models.ModelSettingsState import ee.carlrobert.codegpt.settings.service.FeatureType import ee.carlrobert.codegpt.settings.service.ServiceType import ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettings -import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTAvailableModels import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings import ee.carlrobert.codegpt.settings.service.custom.CustomServicesSettings import ee.carlrobert.codegpt.settings.service.google.GoogleSettings @@ -74,7 +73,7 @@ object LegacySettingsMigration { ServiceType.GOOGLE -> { service().state.model - ?: GoogleModel.GEMINI_PRO.code + ?: GoogleModel.GEMINI_2_5_PRO.code } ServiceType.OLLAMA -> { @@ -96,6 +95,10 @@ object LegacySettingsMigration { .map { it.chatCompletionSettings.body["model"] as String } .lastOrNull() ?: "" } + + ServiceType.MISTRAL -> { + ModelRegistry.CODESTRAL_LATEST + } } } catch (e: Exception) { logger.warn("Could not get legacy model for service $serviceType, using default", e) @@ -140,6 +143,10 @@ object LegacySettingsMigration { .map { it.codeCompletionSettings.body["model"] as String } .lastOrNull() ?: "" } + + ServiceType.MISTRAL -> { + ModelRegistry.CODESTRAL_LATEST + } } } catch (e: Exception) { logger.warn("Could not get legacy model for service $serviceType, using default", e) @@ -153,6 +160,7 @@ object LegacySettingsMigration { ServiceType.OPENAI -> ModelRegistry.GPT_4O ServiceType.ANTHROPIC -> ModelRegistry.CLAUDE_SONNET_4_20250514 ServiceType.GOOGLE -> ModelRegistry.GEMINI_2_0_FLASH + ServiceType.MISTRAL -> ModelRegistry.DEVSTRAL_MEDIUM_2507 ServiceType.OLLAMA -> ModelRegistry.LLAMA_3_2 ServiceType.LLAMA_CPP -> ModelRegistry.LLAMA_3_2_3B_INSTRUCT ServiceType.CUSTOM_OPENAI -> ModelRegistry.GPT_4O diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/models/ModelIcons.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/models/ModelIcons.kt index 07fc605f..e889dc64 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/models/ModelIcons.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/models/ModelIcons.kt @@ -30,6 +30,7 @@ object ModelIcons { ServiceType.OPENAI -> Icons.OpenAI ServiceType.ANTHROPIC -> Icons.Anthropic ServiceType.GOOGLE -> Icons.Google + ServiceType.MISTRAL -> Icons.Mistral ServiceType.OLLAMA -> Icons.Ollama ServiceType.CUSTOM_OPENAI -> Icons.OpenAI ServiceType.LLAMA_CPP -> Icons.Llama diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/models/ModelRegistry.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/models/ModelRegistry.kt index 8110b73a..60a59ab3 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/models/ModelRegistry.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/models/ModelRegistry.kt @@ -8,9 +8,7 @@ import ee.carlrobert.codegpt.Icons import ee.carlrobert.codegpt.completions.llama.LlamaModel import ee.carlrobert.codegpt.settings.service.FeatureType import ee.carlrobert.codegpt.settings.service.ServiceType -import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTAvailableModels import ee.carlrobert.codegpt.settings.service.custom.CustomServicesSettings -import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings import ee.carlrobert.llm.client.codegpt.PricingPlan import ee.carlrobert.llm.client.google.models.GoogleModel @@ -63,6 +61,11 @@ class ModelRegistry { setOf(FeatureType.CHAT, FeatureType.AUTO_APPLY, FeatureType.COMMIT_MESSAGE, FeatureType.EDIT_CODE, FeatureType.LOOKUP) ), + ServiceType.MISTRAL to ModelCapability( + ServiceType.MISTRAL, + setOf(FeatureType.CHAT, FeatureType.CODE_COMPLETION, FeatureType.AUTO_APPLY, + FeatureType.COMMIT_MESSAGE, FeatureType.EDIT_CODE, FeatureType.LOOKUP) + ), ServiceType.OLLAMA to ModelCapability( ServiceType.OLLAMA, setOf(FeatureType.CHAT, FeatureType.CODE_COMPLETION, FeatureType.AUTO_APPLY, @@ -168,6 +171,7 @@ class ModelRegistry { addAll(getOpenAIChatModels()) addAll(getAnthropicModels()) addAll(getGoogleModels()) + addAll(getMistralModels()) addAll(getLlamaModels()) addAll(getOllamaModels()) addAll(getCustomOpenAIModels()) @@ -178,6 +182,7 @@ class ModelRegistry { return buildList { addAll(getProxyAICodeModels()) add(getOpenAICodeModel()) + addAll(getMistralCodeModels()) addAll(getLlamaModels()) addAll(getCustomOpenAICodeModels()) addAll(getOllamaModels()) @@ -207,9 +212,7 @@ class ModelRegistry { } private fun getNextEditModels(): List { - return listOf( - ModelSelection(ServiceType.PROXYAI, ZETA, "Zeta") - ) + return listOf(ModelSelection(ServiceType.PROXYAI, ZETA, "Zeta")) } fun getProxyAIChatModels(): List { @@ -274,7 +277,7 @@ class ModelRegistry { return listOf( ModelSelection(ServiceType.GOOGLE, GoogleModel.GEMINI_2_5_PRO_PREVIEW.code, GoogleModel.GEMINI_2_5_PRO_PREVIEW.description), ModelSelection(ServiceType.GOOGLE, GoogleModel.GEMINI_2_5_FLASH_PREVIEW.code, GoogleModel.GEMINI_2_5_FLASH_PREVIEW.description), - ModelSelection(ServiceType.GOOGLE, GoogleModel.GEMINI_2_5_PRO_EXP.code, GoogleModel.GEMINI_2_5_PRO_EXP.description), + ModelSelection(ServiceType.GOOGLE, GoogleModel.GEMINI_2_5_PRO.code, GoogleModel.GEMINI_2_5_PRO.description), ModelSelection(ServiceType.GOOGLE, GoogleModel.GEMINI_2_0_PRO_EXP.code, GoogleModel.GEMINI_2_0_PRO_EXP.description), ModelSelection(ServiceType.GOOGLE, GoogleModel.GEMINI_2_0_FLASH_THINKING_EXP.code, GoogleModel.GEMINI_2_0_FLASH_THINKING_EXP.description), ModelSelection(ServiceType.GOOGLE, GoogleModel.GEMINI_2_0_FLASH.code, GoogleModel.GEMINI_2_0_FLASH.description), @@ -282,6 +285,18 @@ class ModelRegistry { ) } + private fun getMistralModels(): List { + return listOf( + ModelSelection(ServiceType.MISTRAL, DEVSTRAL_MEDIUM_2507, "Devstral Medium"), + ModelSelection(ServiceType.MISTRAL, MISTRAL_LARGE_2411, "Mistral Large"), + ModelSelection(ServiceType.MISTRAL, CODESTRAL_LATEST, "Codestral"), + ) + } + + private fun getMistralCodeModels(): List { + return listOf(ModelSelection(ServiceType.MISTRAL, CODESTRAL_LATEST, "Codestral")) + } + private fun getOllamaModels(): List { return try { val ollamaSettings = service() @@ -365,12 +380,17 @@ class ModelRegistry { // Google Models const val GEMINI_2_5_PRO_PREVIEW = "gemini-pro-2.5-preview" const val GEMINI_2_5_FLASH_PREVIEW = "gemini-flash-2.5-preview" - const val GEMINI_2_5_PRO_EXP = "gemini-pro-2.5-exp" + const val GEMINI_2_5_PRO = "gemini-2.5-pro" const val GEMINI_2_0_PRO_EXP = "gemini-pro-2.0-exp" const val GEMINI_2_0_FLASH_THINKING_EXP = "gemini-flash-thinking-2.0-exp" const val GEMINI_2_0_FLASH = "gemini-2.0-flash" const val GEMINI_1_5_PRO = "gemini-1.5-pro" + // Mistral Models + const val MISTRAL_LARGE_2411 = "mistral-large-2411" + const val DEVSTRAL_MEDIUM_2507 = "devstral-medium-2507" + const val CODESTRAL_LATEST = "codestral-latest" + // Ollama default models const val LLAMA_3_2 = "llama3.2" diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/models/ModelSettings.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/models/ModelSettings.kt index b7e491f1..43a600d8 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/models/ModelSettings.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/models/ModelSettings.kt @@ -79,13 +79,23 @@ class ModelSettings : SimplePersistentStateComponent(ModelSe } fun getModelSelection(featureType: FeatureType): ModelSelection { - val details = getModelDetailsState(featureType) ?: throw IllegalStateException("No model selected") + val details = getModelDetailsState(featureType) + + if (details == null) { + val defaultModel = ModelRegistry.getInstance().getDefaultModelForFeature(featureType) + state.setModelSelection(featureType, defaultModel.model, defaultModel.provider) + return defaultModel + } return details.model?.let { model -> details.provider?.let { provider -> ModelRegistry.getInstance().findModel(provider, model) } - } ?: throw IllegalStateException("No model found") + } ?: run { + val defaultModel = ModelRegistry.getInstance().getDefaultModelForFeature(featureType) + state.setModelSelection(featureType, defaultModel.model, defaultModel.provider) + defaultModel + } } fun getOrCreateModelSelection(featureType: FeatureType, ): ModelSelection { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/models/SettingsModelComboBoxAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/models/SettingsModelComboBoxAction.kt index 1f193b57..24d03875 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/models/SettingsModelComboBoxAction.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/models/SettingsModelComboBoxAction.kt @@ -67,7 +67,8 @@ class SettingsModelComboBoxAction( ServiceType.ANTHROPIC, ServiceType.OPENAI, ServiceType.CUSTOM_OPENAI, - ServiceType.GOOGLE + ServiceType.GOOGLE, + ServiceType.MISTRAL ) val hasCloudProviders = cloudProviders.any { groupedModels.containsKey(it) } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/MistralServiceConfigurable.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/MistralServiceConfigurable.kt new file mode 100644 index 00000000..71dffbb4 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/MistralServiceConfigurable.kt @@ -0,0 +1,40 @@ +package ee.carlrobert.codegpt.settings.service + +import com.intellij.openapi.components.service +import com.intellij.openapi.options.Configurable +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.MistralApiKey +import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential +import ee.carlrobert.codegpt.credentials.CredentialsStore.setCredential +import ee.carlrobert.codegpt.settings.service.mistral.MistralSettings +import ee.carlrobert.codegpt.settings.service.mistral.MistralSettingsForm +import javax.swing.JComponent + +class MistralServiceConfigurable : Configurable { + + private lateinit var component: MistralSettingsForm + + override fun getDisplayName(): String { + return "ProxyAI: Mistral Service" + } + + override fun createComponent(): JComponent { + component = MistralSettingsForm(service().state) + return component.form + } + + override fun isModified(): Boolean { + return component.getCurrentState() != service().state + || component.getApiKey() != getCredential(MistralApiKey) + } + + override fun apply() { + setCredential(MistralApiKey, component.getApiKey()) + service().loadState(component.getCurrentState()) + + ModelReplacementDialog.showDialogIfNeeded(ServiceType.MISTRAL) + } + + override fun reset() { + component.resetForm() + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ModelSelectionService.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ModelSelectionService.kt index fcbd4066..4f7896d1 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ModelSelectionService.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ModelSelectionService.kt @@ -17,7 +17,15 @@ class ModelSelectionService { pricingPlan: PricingPlan? = null ): ModelSelection { return try { - service().getModelSelection(featureType) + val modelDetailsState = service().state.getModelSelection(featureType) + if (modelDetailsState != null && modelDetailsState.model != null && modelDetailsState.provider != null) { + val foundModel = service().findModel(modelDetailsState.provider!!, modelDetailsState.model!!) + if (foundModel != null) { + return foundModel + } + } + + service().getDefaultModelForFeature(featureType, pricingPlan) } catch (exception: Exception) { logger.warn( "Error getting model selection for feature: $featureType, using default", diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ServiceConfigurableComponent.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ServiceConfigurableComponent.kt index 8df7c84e..cce04278 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ServiceConfigurableComponent.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ServiceConfigurableComponent.kt @@ -35,6 +35,7 @@ class ServiceConfigurableComponent { "Custom OpenAI" to CustomServiceConfigurable::class.java, "Anthropic" to AnthropicServiceConfigurable::class.java, "Google" to GoogleSettingsConfigurable::class.java, + "Mistral" to MistralServiceConfigurable::class.java, "LLaMA C/C++" to LlamaServiceConfigurable::class.java, "Ollama" to OllamaSettingsConfigurable::class.java, ).entries.forEach { (name, configurableClass) -> 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 index 34131de0..8348aa9c 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/google/GoogleSettings.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/google/GoogleSettings.kt @@ -8,5 +8,5 @@ import ee.carlrobert.llm.client.google.models.GoogleModel class GoogleSettings : SimplePersistentStateComponent(GoogleSettingsState()) class GoogleSettingsState : BaseState() { - var model by string(GoogleModel.GEMINI_PRO.code) + var model by string(GoogleModel.GEMINI_2_5_PRO.code) } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 7137ab98..aaea6915 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -42,6 +42,8 @@ instance="ee.carlrobert.codegpt.settings.service.AnthropicServiceConfigurable"/> + + diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index 2cdf6fc9..adafe8b6 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -229,6 +229,7 @@ service.custom.openai.title=Custom OpenAI service.anthropic.title=Anthropic service.azure.title=Azure service.google.title=Google +service.mistral.title=Mistral service.llama.title=LLaMA C/C++ service.ollama.title=Ollama validation.error.model.notExists='%s' is not available, please select another model diff --git a/src/test/kotlin/ee/carlrobert/codegpt/settings/models/ModelRegistryTest.kt b/src/test/kotlin/ee/carlrobert/codegpt/settings/models/ModelRegistryTest.kt index af59ca85..758c7dbb 100644 --- a/src/test/kotlin/ee/carlrobert/codegpt/settings/models/ModelRegistryTest.kt +++ b/src/test/kotlin/ee/carlrobert/codegpt/settings/models/ModelRegistryTest.kt @@ -70,6 +70,7 @@ class ModelRegistryTest : IntegrationTest() { assertThat(result).anyMatch { it.provider == ServiceType.OPENAI && it.model == "gpt-4.1" } assertThat(result).anyMatch { it.provider == ServiceType.ANTHROPIC && it.model == "claude-sonnet-4-20250514" } assertThat(result).anyMatch { it.provider == ServiceType.GOOGLE && it.model.contains("gemini") } + assertThat(result).anyMatch { it.provider == ServiceType.MISTRAL && it.model == "codestral-latest" } assertThat(result).noneMatch { it.provider == ServiceType.ANTHROPIC && it.model == "qwen-2.5-32b-code" } } @@ -78,6 +79,7 @@ class ModelRegistryTest : IntegrationTest() { assertThat(result).anyMatch { it.provider == ServiceType.PROXYAI && it.model == "qwen-2.5-32b-code" } assertThat(result).anyMatch { it.provider == ServiceType.OPENAI && it.model == "gpt-3.5-turbo-instruct" } + assertThat(result).anyMatch { it.provider == ServiceType.MISTRAL && it.model == "codestral-latest" } assertThat(result).noneMatch { it.provider == ServiceType.ANTHROPIC } assertThat(result).noneMatch { it.provider == ServiceType.GOOGLE } } @@ -97,6 +99,7 @@ class ModelRegistryTest : IntegrationTest() { ServiceType.OPENAI, ServiceType.ANTHROPIC, ServiceType.GOOGLE, + ServiceType.MISTRAL, ServiceType.OLLAMA, ServiceType.LLAMA_CPP, ServiceType.CUSTOM_OPENAI @@ -109,6 +112,7 @@ class ModelRegistryTest : IntegrationTest() { assertThat(result).containsExactlyInAnyOrder( ServiceType.PROXYAI, ServiceType.OPENAI, + ServiceType.MISTRAL, ServiceType.OLLAMA, ServiceType.LLAMA_CPP, ServiceType.CUSTOM_OPENAI @@ -158,6 +162,15 @@ class ModelRegistryTest : IntegrationTest() { assertThat(result.displayName).isEqualTo("Claude Sonnet 4") } + fun `test findModel with existing mistral model returns model selection`() { + val result = modelRegistry.findModel(ServiceType.MISTRAL, "codestral-latest") + + assertThat(result).isNotNull + assertThat(result!!.provider).isEqualTo(ServiceType.MISTRAL) + assertThat(result.model).isEqualTo("codestral-latest") + assertThat(result.displayName).isEqualTo("Codestral") + } + fun `test findModel with non-existing model returns null`() { val result = modelRegistry.findModel(ServiceType.OPENAI, "non-existing-model")