feat: add native mistral client support

This commit is contained in:
Carl-Robert Linnupuu 2025-07-21 11:28:02 +01:00
parent a6ff38e52c
commit d50ad140b0
27 changed files with 371 additions and 29 deletions

View file

@ -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"

View file

@ -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();

View file

@ -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;
};
}

View file

@ -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");

View file

@ -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<MistralSettingsState> {
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);
}
}

View file

@ -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 <a href=\"https://console.mistral.ai/api-keys\">Mistral Console</a>.")
.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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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)

View file

@ -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<CodeGPTServiceSettings>().state.codeCompletionSettings.codeCompletionsEnabled
OPENAI -> OpenAISettings.getCurrentState().isCodeCompletionsEnabled
CUSTOM_OPENAI -> service<CustomServicesSettings>().state.active.codeCompletionSettings.codeCompletionsEnabled
MISTRAL -> MistralSettings.getCurrentState().isCodeCompletionsEnabled
LLAMA_CPP -> LlamaSettings.isCodeCompletionsPossible()
OLLAMA -> service<OllamaSettings>().state.codeCompletionsEnabled
else -> false
@ -46,7 +50,8 @@ class CodeCompletionService(private val project: Project) {
infillRequest: InfillRequest,
eventListener: CompletionEventListener<String>
): 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)

View file

@ -112,6 +112,7 @@ class DebouncedCodeCompletionProvider : DebouncedInlineCompletionProvider() {
ServiceType.CUSTOM_OPENAI -> service<CustomServicesSettings>().state.active.codeCompletionSettings.codeCompletionsEnabled
ServiceType.LLAMA_CPP -> LlamaSettings.isCodeCompletionsPossible()
ServiceType.OLLAMA -> service<OllamaSettings>().state.codeCompletionsEnabled
ServiceType.MISTRAL -> true // Mistral supports code completions
ServiceType.ANTHROPIC,
ServiceType.GOOGLE,
null -> false

View file

@ -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()
}

View file

@ -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<ConfigurationSettings>().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
)
}
}

View file

@ -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"
}
}
}

View file

@ -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<GoogleSettings>().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

View file

@ -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

View file

@ -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<ModelSelection> {
return listOf(
ModelSelection(ServiceType.PROXYAI, ZETA, "Zeta")
)
return listOf(ModelSelection(ServiceType.PROXYAI, ZETA, "Zeta"))
}
fun getProxyAIChatModels(): List<ModelSelection> {
@ -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<ModelSelection> {
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<ModelSelection> {
return listOf(ModelSelection(ServiceType.MISTRAL, CODESTRAL_LATEST, "Codestral"))
}
private fun getOllamaModels(): List<ModelSelection> {
return try {
val ollamaSettings = service<OllamaSettings>()
@ -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"

View file

@ -79,13 +79,23 @@ class ModelSettings : SimplePersistentStateComponent<ModelSettingsState>(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 {

View file

@ -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) }

View file

@ -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<MistralSettings>().state)
return component.form
}
override fun isModified(): Boolean {
return component.getCurrentState() != service<MistralSettings>().state
|| component.getApiKey() != getCredential(MistralApiKey)
}
override fun apply() {
setCredential(MistralApiKey, component.getApiKey())
service<MistralSettings>().loadState(component.getCurrentState())
ModelReplacementDialog.showDialogIfNeeded(ServiceType.MISTRAL)
}
override fun reset() {
component.resetForm()
}
}

View file

@ -17,7 +17,15 @@ class ModelSelectionService {
pricingPlan: PricingPlan? = null
): ModelSelection {
return try {
service<ModelSettings>().getModelSelection(featureType)
val modelDetailsState = service<ModelSettings>().state.getModelSelection(featureType)
if (modelDetailsState != null && modelDetailsState.model != null && modelDetailsState.provider != null) {
val foundModel = service<ModelRegistry>().findModel(modelDetailsState.provider!!, modelDetailsState.model!!)
if (foundModel != null) {
return foundModel
}
}
service<ModelRegistry>().getDefaultModelForFeature(featureType, pricingPlan)
} catch (exception: Exception) {
logger.warn(
"Error getting model selection for feature: $featureType, using default",

View file

@ -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) ->

View file

@ -8,5 +8,5 @@ import ee.carlrobert.llm.client.google.models.GoogleModel
class GoogleSettings : SimplePersistentStateComponent<GoogleSettingsState>(GoogleSettingsState())
class GoogleSettingsState : BaseState() {
var model by string(GoogleModel.GEMINI_PRO.code)
var model by string(GoogleModel.GEMINI_2_5_PRO.code)
}

View file

@ -42,6 +42,8 @@
instance="ee.carlrobert.codegpt.settings.service.AnthropicServiceConfigurable"/>
<applicationConfigurable id="settings.codegpt.services.google" parentId="settings.codegpt.services" displayName="Google"
instance="ee.carlrobert.codegpt.settings.service.google.GoogleSettingsConfigurable"/>
<applicationConfigurable id="settings.codegpt.services.mistral" parentId="settings.codegpt.services" displayName="Mistral"
instance="ee.carlrobert.codegpt.settings.service.MistralServiceConfigurable"/>
<applicationConfigurable id="settings.codegpt.services.llama_cpp" parentId="settings.codegpt.services" displayName="LLaMA C/C++ (Offline)"
instance="ee.carlrobert.codegpt.settings.service.LlamaServiceConfigurable"/>
<applicationConfigurable id="settings.codegpt.services.ollama" parentId="settings.codegpt.services" displayName="Ollama (Offline)"
@ -68,6 +70,7 @@
<applicationService serviceImplementation="ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettings"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.settings.service.openai.OpenAISettings"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.settings.service.you.YouSettings"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.settings.service.mistral.MistralSettings"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.settings.service.llama.LlamaSettings"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.settings.models.ModelSettings"/>

View file

@ -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

View file

@ -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")