From e436460bca6daff1b8244c768fcda18c99d1ff54 Mon Sep 17 00:00:00 2001 From: Sannidhya Sah Date: Sat, 19 Jul 2025 18:21:27 +0530 Subject: [PATCH 1/4] feat: add Mistral embedding provider with OpenAI Compatible Wrapper - Implement MistralEmbedder class using OpenAI-compatible API - Add comprehensive unit tests with 100% coverage - Update type definitions for Mistral provider support - Integrate Mistral option in UI components and configuration - Add internationalization support for Mistral provider - Fix API key storage and retrieval for embedding providers - Update service factory to support Mistral embeddings - Add proper error handling and validation This implementation allows users to use Mistral's embedding models through the existing OpenAI-compatible wrapper approach, providing a seamless integration experience. --- .../src/components/chat/CodeIndexPopover.tsx | 85 ++++++++++++++++++- webview-ui/src/i18n/locales/en/settings.json | 4 + 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/webview-ui/src/components/chat/CodeIndexPopover.tsx b/webview-ui/src/components/chat/CodeIndexPopover.tsx index 4385e2e8446..d7683e8c7e6 100644 --- a/webview-ui/src/components/chat/CodeIndexPopover.tsx +++ b/webview-ui/src/components/chat/CodeIndexPopover.tsx @@ -68,6 +68,7 @@ interface LocalCodeIndexSettings { codebaseIndexOpenAiCompatibleBaseUrl?: string codebaseIndexOpenAiCompatibleApiKey?: string codebaseIndexGeminiApiKey?: string + codebaseIndexMistralApiKey?: string } // Validation schema for codebase index settings @@ -126,6 +127,14 @@ const createValidationSchema = (provider: EmbedderProvider, t: any) => { .min(1, t("settings:codeIndex.validation.modelSelectionRequired")), }) + case "mistral": + return baseSchema.extend({ + codebaseIndexMistralApiKey: z.string().min(1, t("settings:codeIndex.validation.mistralApiKeyRequired")), + codebaseIndexEmbedderModelId: z + .string() + .min(1, t("settings:codeIndex.validation.modelSelectionRequired")), + }) + default: return baseSchema } @@ -169,6 +178,7 @@ export const CodeIndexPopover: React.FC = ({ codebaseIndexOpenAiCompatibleBaseUrl: "", codebaseIndexOpenAiCompatibleApiKey: "", codebaseIndexGeminiApiKey: "", + codebaseIndexMistralApiKey: "", }) // Initial settings state - stores the settings when popover opens @@ -202,6 +212,7 @@ export const CodeIndexPopover: React.FC = ({ codebaseIndexOpenAiCompatibleBaseUrl: codebaseIndexConfig.codebaseIndexOpenAiCompatibleBaseUrl || "", codebaseIndexOpenAiCompatibleApiKey: "", codebaseIndexGeminiApiKey: "", + codebaseIndexMistralApiKey: "", } setInitialSettings(settings) setCurrentSettings(settings) @@ -293,6 +304,9 @@ export const CodeIndexPopover: React.FC = ({ if (!prev.codebaseIndexGeminiApiKey || prev.codebaseIndexGeminiApiKey === SECRET_PLACEHOLDER) { updated.codebaseIndexGeminiApiKey = secretStatus.hasGeminiApiKey ? SECRET_PLACEHOLDER : "" } + if (!prev.codebaseIndexMistralApiKey || prev.codebaseIndexMistralApiKey === SECRET_PLACEHOLDER) { + updated.codebaseIndexMistralApiKey = secretStatus.hasMistralApiKey ? SECRET_PLACEHOLDER : "" + } return updated } @@ -364,7 +378,8 @@ export const CodeIndexPopover: React.FC = ({ if ( key === "codeIndexOpenAiKey" || key === "codebaseIndexOpenAiCompatibleApiKey" || - key === "codebaseIndexGeminiApiKey" + key === "codebaseIndexGeminiApiKey" || + key === "codebaseIndexMistralApiKey" ) { dataToValidate[key] = "placeholder-valid" } @@ -606,6 +621,9 @@ export const CodeIndexPopover: React.FC = ({ {t("settings:codeIndex.geminiProvider")} + + {t("settings:codeIndex.mistralProvider")} + @@ -933,6 +951,71 @@ export const CodeIndexPopover: React.FC = ({ )} + {currentSettings.codebaseIndexEmbedderProvider === "mistral" && ( + <> +
+ + + updateSetting("codebaseIndexMistralApiKey", e.target.value) + } + placeholder={t("settings:codeIndex.mistralApiKeyPlaceholder")} + className={cn("w-full", { + "border-red-500": formErrors.codebaseIndexMistralApiKey, + })} + /> + {formErrors.codebaseIndexMistralApiKey && ( +

+ {formErrors.codebaseIndexMistralApiKey} +

+ )} +
+ +
+ + + updateSetting("codebaseIndexEmbedderModelId", e.target.value) + } + className={cn("w-full", { + "border-red-500": formErrors.codebaseIndexEmbedderModelId, + })}> + + {t("settings:codeIndex.selectModel")} + + {getAvailableModels().map((modelId) => { + const model = + codebaseIndexModels?.[ + currentSettings.codebaseIndexEmbedderProvider + ]?.[modelId] + return ( + + {modelId}{" "} + {model + ? t("settings:codeIndex.modelDimensions", { + dimension: model.dimension, + }) + : ""} + + ) + })} + + {formErrors.codebaseIndexEmbedderModelId && ( +

+ {formErrors.codebaseIndexEmbedderModelId} +

+ )} +
+ + )} + {/* Qdrant Settings */}