import { ApiHandlerOptions } from "../../shared/api" import { ContextProxy } from "../../core/config/ContextProxy" import { EmbedderProvider } from "./interfaces/manager" import { CodeIndexConfig, PreviousConfigSnapshot } from "./interfaces/config" import { DEFAULT_SEARCH_MIN_SCORE, DEFAULT_MAX_SEARCH_RESULTS } from "./constants" import { getDefaultModelId, getModelDimension, getModelScoreThreshold } from "../../shared/embeddingModels" /** * Manages configuration state and validation for the code indexing feature. * Handles loading, validating, and providing access to configuration values. */ export class CodeIndexConfigManager { private codebaseIndexEnabled: boolean = false private embedderProvider: EmbedderProvider = "openai" private modelId?: string private modelDimension?: number private openAiOptions?: ApiHandlerOptions private ollamaOptions?: ApiHandlerOptions private openAiCompatibleOptions?: { baseUrl: string; apiKey: string } private geminiOptions?: { apiKey: string } private mistralOptions?: { apiKey: string } private vercelAiGatewayOptions?: { apiKey: string } private bedrockOptions?: { region: string; profile?: string } private openRouterOptions?: { apiKey: string; specificProvider?: string } private qdrantUrl?: string = "http://localhost:6333" private qdrantApiKey?: string private searchMinScore?: number private searchMaxResults?: number private codebaseIndexRespectGitIgnore: boolean = true constructor(private readonly contextProxy: ContextProxy) { // Initialize with current configuration to avoid false restart triggers this._loadAndSetConfiguration() } /** * Gets the context proxy instance */ public getContextProxy(): ContextProxy { return this.contextProxy } /** * Private method that handles loading configuration from storage and updating instance variables. * This eliminates code duplication between initializeWithCurrentConfig() and loadConfiguration(). */ private _loadAndSetConfiguration(): void { // Load configuration from storage const codebaseIndexConfig = this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? { codebaseIndexEnabled: false, codebaseIndexQdrantUrl: "http://localhost:6333", codebaseIndexEmbedderProvider: "openai", codebaseIndexEmbedderBaseUrl: "", codebaseIndexEmbedderModelId: "", codebaseIndexSearchMinScore: undefined, codebaseIndexSearchMaxResults: undefined, codebaseIndexBedrockRegion: "us-east-1", codebaseIndexBedrockProfile: "", } const { codebaseIndexEnabled, codebaseIndexQdrantUrl, codebaseIndexEmbedderProvider, codebaseIndexEmbedderBaseUrl, codebaseIndexEmbedderModelId, codebaseIndexSearchMinScore, codebaseIndexSearchMaxResults, } = codebaseIndexConfig const openAiKey = this.contextProxy?.getSecret("codeIndexOpenAiKey") ?? "" const qdrantApiKey = this.contextProxy?.getSecret("codeIndexQdrantApiKey") ?? "" // Fix: Read OpenAI Compatible settings from the correct location within codebaseIndexConfig const openAiCompatibleBaseUrl = codebaseIndexConfig.codebaseIndexOpenAiCompatibleBaseUrl ?? "" const openAiCompatibleApiKey = this.contextProxy?.getSecret("codebaseIndexOpenAiCompatibleApiKey") ?? "" const geminiApiKey = this.contextProxy?.getSecret("codebaseIndexGeminiApiKey") ?? "" const mistralApiKey = this.contextProxy?.getSecret("codebaseIndexMistralApiKey") ?? "" const vercelAiGatewayApiKey = this.contextProxy?.getSecret("codebaseIndexVercelAiGatewayApiKey") ?? "" const bedrockRegion = codebaseIndexConfig.codebaseIndexBedrockRegion ?? "us-east-1" const bedrockProfile = codebaseIndexConfig.codebaseIndexBedrockProfile ?? "" const openRouterApiKey = this.contextProxy?.getSecret("codebaseIndexOpenRouterApiKey") ?? "" const openRouterSpecificProvider = codebaseIndexConfig.codebaseIndexOpenRouterSpecificProvider ?? "" // Update instance variables with configuration this.codebaseIndexEnabled = codebaseIndexEnabled ?? false this.qdrantUrl = codebaseIndexQdrantUrl this.qdrantApiKey = qdrantApiKey ?? "" this.searchMinScore = codebaseIndexSearchMinScore this.searchMaxResults = codebaseIndexSearchMaxResults this.codebaseIndexRespectGitIgnore = codebaseIndexConfig.codebaseIndexRespectGitIgnore ?? true // Validate and set model dimension const rawDimension = codebaseIndexConfig.codebaseIndexEmbedderModelDimension if (rawDimension !== undefined && rawDimension !== null) { const dimension = Number(rawDimension) if (!isNaN(dimension) && dimension > 0) { this.modelDimension = dimension } else { console.warn( `Invalid codebaseIndexEmbedderModelDimension value: ${rawDimension}. Must be a positive number.`, ) this.modelDimension = undefined } } else { this.modelDimension = undefined } this.openAiOptions = { openAiNativeApiKey: openAiKey } // Set embedder provider with support for openai-compatible if (codebaseIndexEmbedderProvider === "ollama") { this.embedderProvider = "ollama" } else if (codebaseIndexEmbedderProvider === "openai-compatible") { this.embedderProvider = "openai-compatible" } else if (codebaseIndexEmbedderProvider === "gemini") { this.embedderProvider = "gemini" } else if (codebaseIndexEmbedderProvider === "mistral") { this.embedderProvider = "mistral" } else if (codebaseIndexEmbedderProvider === "vercel-ai-gateway") { this.embedderProvider = "vercel-ai-gateway" } else if ((codebaseIndexEmbedderProvider as string) === "bedrock") { this.embedderProvider = "bedrock" } else if (codebaseIndexEmbedderProvider === "openrouter") { this.embedderProvider = "openrouter" } else { this.embedderProvider = "openai" } this.modelId = codebaseIndexEmbedderModelId || undefined this.ollamaOptions = { ollamaBaseUrl: codebaseIndexEmbedderBaseUrl, } this.openAiCompatibleOptions = openAiCompatibleBaseUrl && openAiCompatibleApiKey ? { baseUrl: openAiCompatibleBaseUrl, apiKey: openAiCompatibleApiKey, } : undefined this.geminiOptions = geminiApiKey ? { apiKey: geminiApiKey } : undefined this.mistralOptions = mistralApiKey ? { apiKey: mistralApiKey } : undefined this.vercelAiGatewayOptions = vercelAiGatewayApiKey ? { apiKey: vercelAiGatewayApiKey } : undefined this.openRouterOptions = openRouterApiKey ? { apiKey: openRouterApiKey, specificProvider: openRouterSpecificProvider || undefined } : undefined // Set bedrockOptions if region is provided (profile is optional) this.bedrockOptions = bedrockRegion ? { region: bedrockRegion, profile: bedrockProfile || undefined } : undefined } /** * Loads persisted configuration from globalState. */ public async loadConfiguration(): Promise<{ configSnapshot: PreviousConfigSnapshot currentConfig: { isConfigured: boolean embedderProvider: EmbedderProvider modelId?: string modelDimension?: number openAiOptions?: ApiHandlerOptions ollamaOptions?: ApiHandlerOptions openAiCompatibleOptions?: { baseUrl: string; apiKey: string } geminiOptions?: { apiKey: string } mistralOptions?: { apiKey: string } vercelAiGatewayOptions?: { apiKey: string } bedrockOptions?: { region: string; profile?: string } openRouterOptions?: { apiKey: string } qdrantUrl?: string qdrantApiKey?: string searchMinScore?: number } requiresRestart: boolean }> { // Capture the ACTUAL previous state before loading new configuration const previousConfigSnapshot: PreviousConfigSnapshot = { enabled: this.codebaseIndexEnabled, configured: this.isConfigured(), embedderProvider: this.embedderProvider, modelId: this.modelId, modelDimension: this.modelDimension, openAiKey: this.openAiOptions?.openAiNativeApiKey ?? "", ollamaBaseUrl: this.ollamaOptions?.ollamaBaseUrl ?? "", openAiCompatibleBaseUrl: this.openAiCompatibleOptions?.baseUrl ?? "", openAiCompatibleApiKey: this.openAiCompatibleOptions?.apiKey ?? "", geminiApiKey: this.geminiOptions?.apiKey ?? "", mistralApiKey: this.mistralOptions?.apiKey ?? "", vercelAiGatewayApiKey: this.vercelAiGatewayOptions?.apiKey ?? "", bedrockRegion: this.bedrockOptions?.region ?? "", bedrockProfile: this.bedrockOptions?.profile ?? "", openRouterApiKey: this.openRouterOptions?.apiKey ?? "", openRouterSpecificProvider: this.openRouterOptions?.specificProvider ?? "", qdrantUrl: this.qdrantUrl ?? "", qdrantApiKey: this.qdrantApiKey ?? "", codebaseIndexRespectGitIgnore: this.codebaseIndexRespectGitIgnore, } // Refresh secrets from VSCode storage to ensure we have the latest values await this.contextProxy.refreshSecrets() // Load new configuration from storage and update instance variables this._loadAndSetConfiguration() const requiresRestart = this.doesConfigChangeRequireRestart(previousConfigSnapshot) return { configSnapshot: previousConfigSnapshot, currentConfig: { isConfigured: this.isConfigured(), embedderProvider: this.embedderProvider, modelId: this.modelId, modelDimension: this.modelDimension, openAiOptions: this.openAiOptions, ollamaOptions: this.ollamaOptions, openAiCompatibleOptions: this.openAiCompatibleOptions, geminiOptions: this.geminiOptions, mistralOptions: this.mistralOptions, vercelAiGatewayOptions: this.vercelAiGatewayOptions, bedrockOptions: this.bedrockOptions, openRouterOptions: this.openRouterOptions, qdrantUrl: this.qdrantUrl, qdrantApiKey: this.qdrantApiKey, searchMinScore: this.currentSearchMinScore, }, requiresRestart, } } /** * Checks if the service is properly configured based on the embedder type. */ public isConfigured(): boolean { if (this.embedderProvider === "openai") { const openAiKey = this.openAiOptions?.openAiNativeApiKey const qdrantUrl = this.qdrantUrl return !!(openAiKey && qdrantUrl) } else if (this.embedderProvider === "ollama") { // Ollama model ID has a default, so only base URL is strictly required for config const ollamaBaseUrl = this.ollamaOptions?.ollamaBaseUrl const qdrantUrl = this.qdrantUrl return !!(ollamaBaseUrl && qdrantUrl) } else if (this.embedderProvider === "openai-compatible") { const baseUrl = this.openAiCompatibleOptions?.baseUrl const apiKey = this.openAiCompatibleOptions?.apiKey const qdrantUrl = this.qdrantUrl const isConfigured = !!(baseUrl && apiKey && qdrantUrl) return isConfigured } else if (this.embedderProvider === "gemini") { const apiKey = this.geminiOptions?.apiKey const qdrantUrl = this.qdrantUrl const isConfigured = !!(apiKey && qdrantUrl) return isConfigured } else if (this.embedderProvider === "mistral") { const apiKey = this.mistralOptions?.apiKey const qdrantUrl = this.qdrantUrl const isConfigured = !!(apiKey && qdrantUrl) return isConfigured } else if (this.embedderProvider === "vercel-ai-gateway") { const apiKey = this.vercelAiGatewayOptions?.apiKey const qdrantUrl = this.qdrantUrl const isConfigured = !!(apiKey && qdrantUrl) return isConfigured } else if (this.embedderProvider === "bedrock") { // Only region is required for Bedrock (profile is optional) const region = this.bedrockOptions?.region const qdrantUrl = this.qdrantUrl const isConfigured = !!(region && qdrantUrl) return isConfigured } else if (this.embedderProvider === "openrouter") { const apiKey = this.openRouterOptions?.apiKey const qdrantUrl = this.qdrantUrl const isConfigured = !!(apiKey && qdrantUrl) return isConfigured } return false // Should not happen if embedderProvider is always set correctly } /** * Determines if a configuration change requires restarting the indexing process. * Simplified logic: only restart for critical changes that affect service functionality. * * CRITICAL CHANGES (require restart): * - Provider changes (openai -> ollama, etc.) * - Authentication changes (API keys, base URLs) * - Vector dimension changes (model changes that affect embedding size) * - Qdrant connection changes (URL, API key) * - Feature enable/disable transitions * * MINOR CHANGES (no restart needed): * - Search minimum score adjustments * - UI-only settings * - Non-functional configuration tweaks */ doesConfigChangeRequireRestart(prev: PreviousConfigSnapshot): boolean { const nowConfigured = this.isConfigured() // Handle null/undefined values safely const prevEnabled = prev?.enabled ?? false const prevConfigured = prev?.configured ?? false const prevProvider = prev?.embedderProvider ?? "openai" const prevOpenAiKey = prev?.openAiKey ?? "" const prevOllamaBaseUrl = prev?.ollamaBaseUrl ?? "" const prevOpenAiCompatibleBaseUrl = prev?.openAiCompatibleBaseUrl ?? "" const prevOpenAiCompatibleApiKey = prev?.openAiCompatibleApiKey ?? "" const prevModelDimension = prev?.modelDimension const prevGeminiApiKey = prev?.geminiApiKey ?? "" const prevMistralApiKey = prev?.mistralApiKey ?? "" const prevVercelAiGatewayApiKey = prev?.vercelAiGatewayApiKey ?? "" const prevBedrockRegion = prev?.bedrockRegion ?? "" const prevBedrockProfile = prev?.bedrockProfile ?? "" const prevOpenRouterApiKey = prev?.openRouterApiKey ?? "" const prevOpenRouterSpecificProvider = prev?.openRouterSpecificProvider ?? "" const prevQdrantUrl = prev?.qdrantUrl ?? "" const prevQdrantApiKey = prev?.qdrantApiKey ?? "" // 1. Transition from disabled/unconfigured to enabled/configured if ((!prevEnabled || !prevConfigured) && this.codebaseIndexEnabled && nowConfigured) { return true } // 2. Transition from enabled to disabled if (prevEnabled && !this.codebaseIndexEnabled) { return true } // 3. If wasn't ready before and isn't ready now, no restart needed if ((!prevEnabled || !prevConfigured) && (!this.codebaseIndexEnabled || !nowConfigured)) { return false } // 4. CRITICAL CHANGES - Always restart for these // Only check for critical changes if feature is enabled if (!this.codebaseIndexEnabled) { return false } // Provider change if (prevProvider !== this.embedderProvider) { return true } // Authentication changes (API keys) const currentOpenAiKey = this.openAiOptions?.openAiNativeApiKey ?? "" const currentOllamaBaseUrl = this.ollamaOptions?.ollamaBaseUrl ?? "" const currentOpenAiCompatibleBaseUrl = this.openAiCompatibleOptions?.baseUrl ?? "" const currentOpenAiCompatibleApiKey = this.openAiCompatibleOptions?.apiKey ?? "" const currentModelDimension = this.modelDimension const currentGeminiApiKey = this.geminiOptions?.apiKey ?? "" const currentMistralApiKey = this.mistralOptions?.apiKey ?? "" const currentVercelAiGatewayApiKey = this.vercelAiGatewayOptions?.apiKey ?? "" const currentBedrockRegion = this.bedrockOptions?.region ?? "" const currentBedrockProfile = this.bedrockOptions?.profile ?? "" const currentOpenRouterApiKey = this.openRouterOptions?.apiKey ?? "" const currentOpenRouterSpecificProvider = this.openRouterOptions?.specificProvider ?? "" const currentQdrantUrl = this.qdrantUrl ?? "" const currentQdrantApiKey = this.qdrantApiKey ?? "" if (prevOpenAiKey !== currentOpenAiKey) { return true } if (prevOllamaBaseUrl !== currentOllamaBaseUrl) { return true } if ( prevOpenAiCompatibleBaseUrl !== currentOpenAiCompatibleBaseUrl || prevOpenAiCompatibleApiKey !== currentOpenAiCompatibleApiKey ) { return true } if (prevGeminiApiKey !== currentGeminiApiKey) { return true } if (prevMistralApiKey !== currentMistralApiKey) { return true } if (prevVercelAiGatewayApiKey !== currentVercelAiGatewayApiKey) { return true } if (prevBedrockRegion !== currentBedrockRegion || prevBedrockProfile !== currentBedrockProfile) { return true } if (prevOpenRouterApiKey !== currentOpenRouterApiKey) { return true } // OpenRouter specific provider change if (prevOpenRouterSpecificProvider !== currentOpenRouterSpecificProvider) { return true } // Check for model dimension changes (generic for all providers) if (prevModelDimension !== currentModelDimension) { return true } if (prevQdrantUrl !== currentQdrantUrl || prevQdrantApiKey !== currentQdrantApiKey) { return true } // Vector dimension changes (still important for compatibility) if (this._hasVectorDimensionChanged(prevProvider, prev?.modelId)) { return true } // codebaseIndexRespectGitIgnore change const prevRespectGitIgnore = prev?.codebaseIndexRespectGitIgnore ?? true if (prevRespectGitIgnore !== this.codebaseIndexRespectGitIgnore) { return true } return false } /** * Checks if model changes result in vector dimension changes that require restart. */ private _hasVectorDimensionChanged(prevProvider: EmbedderProvider, prevModelId?: string): boolean { const currentProvider = this.embedderProvider const currentModelId = this.modelId ?? getDefaultModelId(currentProvider) const resolvedPrevModelId = prevModelId ?? getDefaultModelId(prevProvider) // If model IDs are the same and provider is the same, no dimension change if (prevProvider === currentProvider && resolvedPrevModelId === currentModelId) { return false } // Get vector dimensions for both models const prevDimension = getModelDimension(prevProvider, resolvedPrevModelId) const currentDimension = getModelDimension(currentProvider, currentModelId) // If we can't determine dimensions, be safe and restart if (prevDimension === undefined || currentDimension === undefined) { return true } // Only restart if dimensions actually changed return prevDimension !== currentDimension } /** * Gets the current configuration state. */ public getConfig(): CodeIndexConfig { return { isConfigured: this.isConfigured(), embedderProvider: this.embedderProvider, modelId: this.modelId, modelDimension: this.modelDimension, openAiOptions: this.openAiOptions, ollamaOptions: this.ollamaOptions, openAiCompatibleOptions: this.openAiCompatibleOptions, geminiOptions: this.geminiOptions, mistralOptions: this.mistralOptions, vercelAiGatewayOptions: this.vercelAiGatewayOptions, bedrockOptions: this.bedrockOptions, openRouterOptions: this.openRouterOptions, qdrantUrl: this.qdrantUrl, qdrantApiKey: this.qdrantApiKey, searchMinScore: this.currentSearchMinScore, searchMaxResults: this.currentSearchMaxResults, } } /** * Gets whether the code indexing feature is enabled */ public get isFeatureEnabled(): boolean { return this.codebaseIndexEnabled } /** * Gets whether the code indexing feature is properly configured */ public get isFeatureConfigured(): boolean { return this.isConfigured() } /** * Gets the current embedder type (openai or ollama) */ public get currentEmbedderProvider(): EmbedderProvider { return this.embedderProvider } /** * Gets the current Qdrant configuration */ public get qdrantConfig(): { url?: string; apiKey?: string } { return { url: this.qdrantUrl, apiKey: this.qdrantApiKey, } } /** * Gets the current model ID being used for embeddings. */ public get currentModelId(): string | undefined { return this.modelId } /** * Gets the current model dimension being used for embeddings. * Returns the model's built-in dimension if available, otherwise falls back to custom dimension. */ public get currentModelDimension(): number | undefined { // First try to get the model-specific dimension const modelId = this.modelId ?? getDefaultModelId(this.embedderProvider) const modelDimension = getModelDimension(this.embedderProvider, modelId) // Only use custom dimension if model doesn't have a built-in dimension if (!modelDimension && this.modelDimension && this.modelDimension > 0) { return this.modelDimension } return modelDimension } /** * Gets the configured minimum search score based on user setting, model-specific threshold, or fallback. * Priority: 1) User setting, 2) Model-specific threshold, 3) Default DEFAULT_SEARCH_MIN_SCORE constant. */ public get currentSearchMinScore(): number { // First check if user has configured a custom score threshold if (this.searchMinScore !== undefined) { return this.searchMinScore } // Fall back to model-specific threshold const currentModelId = this.modelId ?? getDefaultModelId(this.embedderProvider) const modelSpecificThreshold = getModelScoreThreshold(this.embedderProvider, currentModelId) return modelSpecificThreshold ?? DEFAULT_SEARCH_MIN_SCORE } /** * Gets the configured maximum search results. * Returns user setting if configured, otherwise returns default. */ public get currentSearchMaxResults(): number { return this.searchMaxResults ?? DEFAULT_MAX_SEARCH_RESULTS } }