Browse Source

fix: correct export/import of OpenAI Compatible codebase indexing set… (#5383)

Murilo Pires 6 months ago
parent
commit
6ec017ca65

+ 4 - 0
packages/types/src/codebase-index.ts

@@ -24,12 +24,16 @@ export const codebaseIndexConfigSchema = z.object({
 	codebaseIndexEmbedderProvider: z.enum(["openai", "ollama", "openai-compatible", "gemini"]).optional(),
 	codebaseIndexEmbedderBaseUrl: z.string().optional(),
 	codebaseIndexEmbedderModelId: z.string().optional(),
+	codebaseIndexEmbedderModelDimension: z.number().optional(),
 	codebaseIndexSearchMinScore: z.number().min(0).max(1).optional(),
 	codebaseIndexSearchMaxResults: z
 		.number()
 		.min(CODEBASE_INDEX_DEFAULTS.MIN_SEARCH_RESULTS)
 		.max(CODEBASE_INDEX_DEFAULTS.MAX_SEARCH_RESULTS)
 		.optional(),
+	// OpenAI Compatible specific fields
+	codebaseIndexOpenAiCompatibleBaseUrl: z.string().optional(),
+	codebaseIndexOpenAiCompatibleModelDimension: z.number().optional(),
 })
 
 export type CodebaseIndexConfig = z.infer<typeof codebaseIndexConfigSchema>

+ 1019 - 0
src/core/config/__tests__/importExport.spec.ts

@@ -595,5 +595,1024 @@ describe("importExport", () => {
 
 			expect(vscode.Uri.file).toHaveBeenCalledWith(path.join("/mock/home", "Documents", "roo-code-settings.json"))
 		})
+
+		describe("codebase indexing export", () => {
+			it("should export correct base URL for OpenAI Compatible provider", async () => {
+				;(vscode.window.showSaveDialog as Mock).mockResolvedValue({
+					fsPath: "/mock/path/roo-code-settings.json",
+				})
+
+				const mockProviderProfiles = {
+					currentApiConfigName: "openai-compatible-provider",
+					apiConfigs: {
+						"openai-compatible-provider": {
+							apiProvider: "openai" as ProviderName,
+							id: "openai-compatible-id",
+							// Remove OpenAI Compatible settings from provider profile
+						},
+						"ollama-provider": {
+							apiProvider: "ollama" as ProviderName,
+							id: "ollama-id",
+							codebaseIndexOllamaBaseUrl: "http://localhost:11434",
+						},
+					},
+					modeApiConfigs: {},
+				}
+
+				const mockGlobalSettings = {
+					mode: "code",
+					codebaseIndexConfig: {
+						codebaseIndexEnabled: true,
+						codebaseIndexEmbedderProvider: "openai-compatible" as const,
+						codebaseIndexEmbedderModelId: "text-embedding-3-small",
+						codebaseIndexEmbedderBaseUrl: "http://localhost:11434", // Wrong URL from Ollama
+						// OpenAI Compatible settings are now stored directly in codebaseIndexConfig
+						codebaseIndexOpenAiCompatibleBaseUrl: "https://custom-openai-api.example.com/v1",
+						codebaseIndexOpenAiCompatibleModelDimension: 1536,
+					},
+				}
+
+				mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles)
+				mockContextProxy.export.mockResolvedValue(mockGlobalSettings)
+				;(fs.mkdir as Mock).mockResolvedValue(undefined)
+
+				await exportSettings({
+					providerSettingsManager: mockProviderSettingsManager,
+					contextProxy: mockContextProxy,
+				})
+
+				expect(safeWriteJson).toHaveBeenCalledWith("/mock/path/roo-code-settings.json", {
+					providerProfiles: mockProviderProfiles,
+					globalSettings: mockGlobalSettings,
+				})
+			})
+
+			it("should export model dimension for OpenAI Compatible provider", async () => {
+				;(vscode.window.showSaveDialog as Mock).mockResolvedValue({
+					fsPath: "/mock/path/roo-code-settings.json",
+				})
+
+				const mockProviderProfiles = {
+					currentApiConfigName: "test-provider",
+					apiConfigs: {
+						"test-provider": {
+							apiProvider: "openai" as ProviderName,
+							id: "test-id",
+							// Remove OpenAI Compatible settings from provider profile
+						},
+					},
+					modeApiConfigs: {},
+				}
+
+				const mockGlobalSettings = {
+					mode: "code",
+					codebaseIndexConfig: {
+						codebaseIndexEnabled: true,
+						codebaseIndexEmbedderProvider: "openai-compatible" as const,
+						codebaseIndexEmbedderModelId: "custom-embedding-model",
+						codebaseIndexEmbedderBaseUrl: "",
+						// OpenAI Compatible settings are now stored directly in codebaseIndexConfig
+						codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1",
+						codebaseIndexOpenAiCompatibleModelDimension: 768,
+					},
+				}
+
+				mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles)
+				mockContextProxy.export.mockResolvedValue(mockGlobalSettings)
+				;(fs.mkdir as Mock).mockResolvedValue(undefined)
+
+				await exportSettings({
+					providerSettingsManager: mockProviderSettingsManager,
+					contextProxy: mockContextProxy,
+				})
+
+				const exportedData = (safeWriteJson as Mock).mock.calls[0][1]
+				// Settings are now exported as-is from codebaseIndexConfig
+				expect(
+					exportedData.globalSettings.codebaseIndexConfig.codebaseIndexOpenAiCompatibleModelDimension,
+				).toBe(768)
+				expect(exportedData.globalSettings.codebaseIndexConfig.codebaseIndexOpenAiCompatibleBaseUrl).toBe(
+					"https://api.example.com/v1",
+				)
+			})
+
+			it("should not mix settings between different providers", async () => {
+				;(vscode.window.showSaveDialog as Mock).mockResolvedValue({
+					fsPath: "/mock/path/roo-code-settings.json",
+				})
+
+				const mockProviderProfiles = {
+					currentApiConfigName: "openai-compatible-provider",
+					apiConfigs: {
+						"openai-compatible-provider": {
+							apiProvider: "openai" as ProviderName,
+							id: "openai-compatible-id",
+							// Remove OpenAI Compatible settings from provider profile
+						},
+						"ollama-provider": {
+							apiProvider: "ollama" as ProviderName,
+							id: "ollama-id",
+							codebaseIndexOllamaBaseUrl: "http://localhost:11434",
+						},
+						"anthropic-provider": {
+							apiProvider: "anthropic" as ProviderName,
+							id: "anthropic-id",
+						},
+					},
+					modeApiConfigs: {},
+				}
+
+				const mockGlobalSettings = {
+					mode: "code",
+					codebaseIndexConfig: {
+						codebaseIndexEnabled: true,
+						codebaseIndexEmbedderProvider: "openai-compatible" as const,
+						codebaseIndexEmbedderModelId: "text-embedding-3-small",
+						codebaseIndexEmbedderBaseUrl: "http://localhost:11434", // Wrong URL from Ollama
+						// OpenAI Compatible settings are now stored directly in codebaseIndexConfig
+						codebaseIndexOpenAiCompatibleBaseUrl: "https://openai-compatible.example.com/v1",
+						codebaseIndexOpenAiCompatibleModelDimension: 1536,
+					},
+				}
+
+				mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles)
+				mockContextProxy.export.mockResolvedValue(mockGlobalSettings)
+				;(fs.mkdir as Mock).mockResolvedValue(undefined)
+
+				await exportSettings({
+					providerSettingsManager: mockProviderSettingsManager,
+					contextProxy: mockContextProxy,
+				})
+
+				const exportedData = (safeWriteJson as Mock).mock.calls[0][1]
+				// Settings are now exported as-is from codebaseIndexConfig
+				expect(exportedData.globalSettings.codebaseIndexConfig.codebaseIndexOpenAiCompatibleBaseUrl).toBe(
+					"https://openai-compatible.example.com/v1",
+				)
+				expect(
+					exportedData.globalSettings.codebaseIndexConfig.codebaseIndexOpenAiCompatibleModelDimension,
+				).toBe(1536)
+				// The generic embedder base URL is still there
+				expect(exportedData.globalSettings.codebaseIndexConfig.codebaseIndexEmbedderBaseUrl).toBe(
+					"http://localhost:11434",
+				)
+			})
+
+			it("should handle missing provider-specific settings gracefully", async () => {
+				;(vscode.window.showSaveDialog as Mock).mockResolvedValue({
+					fsPath: "/mock/path/roo-code-settings.json",
+				})
+
+				const mockProviderProfiles = {
+					currentApiConfigName: "incomplete-provider",
+					apiConfigs: {
+						"incomplete-provider": {
+							apiProvider: "openai" as ProviderName,
+							id: "incomplete-id",
+							// Missing codebaseIndexOpenAiCompatibleBaseUrl and dimension
+						},
+					},
+					modeApiConfigs: {},
+				}
+
+				const mockGlobalSettings = {
+					mode: "code",
+					codebaseIndexConfig: {
+						codebaseIndexEnabled: true,
+						codebaseIndexEmbedderProvider: "openai-compatible" as const,
+						codebaseIndexEmbedderModelId: "text-embedding-3-small",
+						codebaseIndexEmbedderBaseUrl: "https://fallback.example.com/v1",
+					},
+				}
+
+				// Mock getGlobalState to return undefined (no settings)
+				mockContextProxy.getGlobalState = vi.fn().mockReturnValue(undefined)
+
+				mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles)
+				mockContextProxy.export.mockResolvedValue(mockGlobalSettings)
+				;(fs.mkdir as Mock).mockResolvedValue(undefined)
+
+				await exportSettings({
+					providerSettingsManager: mockProviderSettingsManager,
+					contextProxy: mockContextProxy,
+				})
+
+				// Should not throw an error and should preserve original settings
+				expect(safeWriteJson).toHaveBeenCalledWith("/mock/path/roo-code-settings.json", {
+					providerProfiles: mockProviderProfiles,
+					globalSettings: mockGlobalSettings, // Should remain unchanged
+				})
+			})
+
+			it("should maintain backward compatibility with existing exports", async () => {
+				;(vscode.window.showSaveDialog as Mock).mockResolvedValue({
+					fsPath: "/mock/path/roo-code-settings.json",
+				})
+
+				const mockProviderProfiles = {
+					currentApiConfigName: "openai-provider",
+					apiConfigs: {
+						"openai-provider": {
+							apiProvider: "openai" as ProviderName,
+							id: "openai-id",
+							// Regular OpenAI provider without OpenAI Compatible settings
+						},
+					},
+					modeApiConfigs: {},
+				}
+
+				const mockGlobalSettings = {
+					mode: "code",
+					codebaseIndexConfig: {
+						codebaseIndexEnabled: true,
+						codebaseIndexEmbedderProvider: "openai" as const, // Not openai-compatible
+						codebaseIndexEmbedderModelId: "text-embedding-ada-002",
+						codebaseIndexEmbedderBaseUrl: "https://api.openai.com/v1",
+					},
+				}
+
+				mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles)
+				mockContextProxy.export.mockResolvedValue(mockGlobalSettings)
+				;(fs.mkdir as Mock).mockResolvedValue(undefined)
+
+				await exportSettings({
+					providerSettingsManager: mockProviderSettingsManager,
+					contextProxy: mockContextProxy,
+				})
+
+				// Should not modify settings for non-openai-compatible providers
+				expect(safeWriteJson).toHaveBeenCalledWith("/mock/path/roo-code-settings.json", {
+					providerProfiles: mockProviderProfiles,
+					globalSettings: mockGlobalSettings, // Should remain unchanged
+				})
+			})
+
+			it("should handle missing current provider gracefully", async () => {
+				;(vscode.window.showSaveDialog as Mock).mockResolvedValue({
+					fsPath: "/mock/path/roo-code-settings.json",
+				})
+
+				const mockProviderProfiles = {
+					currentApiConfigName: "nonexistent-provider",
+					apiConfigs: {
+						"other-provider": {
+							apiProvider: "openai" as ProviderName,
+							id: "other-id",
+						},
+					},
+					modeApiConfigs: {},
+				}
+
+				const mockGlobalSettings = {
+					mode: "code",
+					codebaseIndexConfig: {
+						codebaseIndexEnabled: true,
+						codebaseIndexEmbedderProvider: "openai-compatible" as const,
+						codebaseIndexEmbedderModelId: "text-embedding-3-small",
+						codebaseIndexEmbedderBaseUrl: "https://fallback.example.com/v1",
+					},
+				}
+
+				// Mock getGlobalState to return undefined (no settings)
+				mockContextProxy.getGlobalState = vi.fn().mockReturnValue(undefined)
+
+				mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles)
+				mockContextProxy.export.mockResolvedValue(mockGlobalSettings)
+				;(fs.mkdir as Mock).mockResolvedValue(undefined)
+
+				await exportSettings({
+					providerSettingsManager: mockProviderSettingsManager,
+					contextProxy: mockContextProxy,
+				})
+
+				// Should not throw an error and should preserve original settings
+				expect(safeWriteJson).toHaveBeenCalledWith("/mock/path/roo-code-settings.json", {
+					providerProfiles: mockProviderProfiles,
+					globalSettings: mockGlobalSettings, // Should remain unchanged
+				})
+			})
+		})
+
+		describe("import with OpenAI Compatible codebase indexing settings", () => {
+			it("should properly import OpenAI Compatible settings in codebaseIndexConfig", async () => {
+				;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
+
+				const mockFileContent = JSON.stringify({
+					providerProfiles: {
+						currentApiConfigName: "openai-compatible-provider",
+						apiConfigs: {
+							"openai-compatible-provider": {
+								apiProvider: "openai" as ProviderName,
+								id: "openai-compatible-id",
+								// Provider-specific settings remain in provider profile
+								codebaseIndexOpenAiCompatibleBaseUrl: "https://old-url.example.com/v1",
+								codebaseIndexOpenAiCompatibleModelDimension: 512,
+							},
+						},
+						modeApiConfigs: {},
+					},
+					globalSettings: {
+						mode: "code",
+						codebaseIndexConfig: {
+							codebaseIndexEnabled: true,
+							codebaseIndexEmbedderProvider: "openai-compatible" as const,
+							codebaseIndexEmbedderModelId: "text-embedding-3-small",
+							codebaseIndexEmbedderBaseUrl: "https://imported-url.example.com/v1",
+							codebaseIndexEmbedderModelDimension: 1536,
+							// OpenAI Compatible settings are now stored directly here
+							codebaseIndexOpenAiCompatibleBaseUrl: "https://imported-url.example.com/v1",
+							codebaseIndexOpenAiCompatibleModelDimension: 1536,
+						},
+					},
+				})
+
+				;(fs.readFile as Mock).mockResolvedValue(mockFileContent)
+
+				const previousProviderProfiles = {
+					currentApiConfigName: "default",
+					apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } },
+				}
+
+				mockProviderSettingsManager.export.mockResolvedValue(previousProviderProfiles)
+				mockProviderSettingsManager.listConfig.mockResolvedValue([
+					{
+						name: "openai-compatible-provider",
+						id: "openai-compatible-id",
+						apiProvider: "openai" as ProviderName,
+					},
+					{ name: "default", id: "default-id", apiProvider: "anthropic" as ProviderName },
+				])
+
+				const result = await importSettings({
+					providerSettingsManager: mockProviderSettingsManager,
+					contextProxy: mockContextProxy,
+					customModesManager: mockCustomModesManager,
+				})
+
+				expect(result.success).toBe(true)
+
+				// Verify that the global settings were imported correctly
+				expect(mockContextProxy.setValues).toHaveBeenCalledWith(
+					expect.objectContaining({
+						codebaseIndexConfig: expect.objectContaining({
+							codebaseIndexOpenAiCompatibleBaseUrl: "https://imported-url.example.com/v1",
+							codebaseIndexOpenAiCompatibleModelDimension: 1536,
+						}),
+					}),
+				)
+
+				// Provider profiles are imported as-is
+				const importedProviderProfiles = mockProviderSettingsManager.import.mock.calls[0][0]
+				const importedProvider = importedProviderProfiles.apiConfigs["openai-compatible-provider"]
+
+				// Provider still has its own settings (not modified by import)
+				expect(importedProvider.codebaseIndexOpenAiCompatibleBaseUrl).toBe("https://old-url.example.com/v1")
+				expect(importedProvider.codebaseIndexOpenAiCompatibleModelDimension).toBe(512)
+			})
+
+			it("should handle missing OpenAI Compatible settings gracefully during import", async () => {
+				;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
+
+				const mockFileContent = JSON.stringify({
+					providerProfiles: {
+						currentApiConfigName: "openai-compatible-provider",
+						apiConfigs: {
+							"openai-compatible-provider": {
+								apiProvider: "openai" as ProviderName,
+								id: "openai-compatible-id",
+							},
+						},
+						modeApiConfigs: {},
+					},
+					globalSettings: {
+						mode: "code",
+						codebaseIndexConfig: {
+							codebaseIndexEnabled: true,
+							codebaseIndexEmbedderProvider: "openai-compatible" as const,
+							codebaseIndexEmbedderModelId: "text-embedding-3-small",
+							// Missing base URL and model dimension
+						},
+					},
+				})
+
+				;(fs.readFile as Mock).mockResolvedValue(mockFileContent)
+
+				const previousProviderProfiles = {
+					currentApiConfigName: "default",
+					apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } },
+				}
+
+				mockProviderSettingsManager.export.mockResolvedValue(previousProviderProfiles)
+				mockProviderSettingsManager.listConfig.mockResolvedValue([
+					{
+						name: "openai-compatible-provider",
+						id: "openai-compatible-id",
+						apiProvider: "openai" as ProviderName,
+					},
+				])
+
+				const result = await importSettings({
+					providerSettingsManager: mockProviderSettingsManager,
+					contextProxy: mockContextProxy,
+					customModesManager: mockCustomModesManager,
+				})
+
+				expect(result.success).toBe(true)
+				// Should not throw an error when settings are missing
+			})
+
+			it("should not modify provider settings for non-openai-compatible providers during import", async () => {
+				;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
+
+				const mockFileContent = JSON.stringify({
+					providerProfiles: {
+						currentApiConfigName: "anthropic-provider",
+						apiConfigs: {
+							"anthropic-provider": {
+								apiProvider: "anthropic" as ProviderName,
+								id: "anthropic-id",
+							},
+						},
+						modeApiConfigs: {},
+					},
+					globalSettings: {
+						mode: "code",
+						codebaseIndexConfig: {
+							codebaseIndexEnabled: true,
+							codebaseIndexEmbedderProvider: "openai" as const, // Not openai-compatible
+							codebaseIndexEmbedderModelId: "text-embedding-ada-002",
+							codebaseIndexEmbedderBaseUrl: "https://api.openai.com/v1",
+							codebaseIndexEmbedderModelDimension: 1536,
+						},
+					},
+				})
+
+				;(fs.readFile as Mock).mockResolvedValue(mockFileContent)
+
+				const previousProviderProfiles = {
+					currentApiConfigName: "default",
+					apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } },
+				}
+
+				mockProviderSettingsManager.export.mockResolvedValue(previousProviderProfiles)
+				mockProviderSettingsManager.listConfig.mockResolvedValue([
+					{ name: "anthropic-provider", id: "anthropic-id", apiProvider: "anthropic" as ProviderName },
+				])
+
+				const result = await importSettings({
+					providerSettingsManager: mockProviderSettingsManager,
+					contextProxy: mockContextProxy,
+					customModesManager: mockCustomModesManager,
+				})
+
+				expect(result.success).toBe(true)
+
+				// Verify that the provider settings were not modified with OpenAI Compatible fields
+				const importedProviderProfiles = mockProviderSettingsManager.import.mock.calls[0][0]
+				const importedProvider = importedProviderProfiles.apiConfigs["anthropic-provider"]
+
+				expect(importedProvider.codebaseIndexOpenAiCompatibleBaseUrl).toBeUndefined()
+				expect(importedProvider.codebaseIndexOpenAiCompatibleModelDimension).toBeUndefined()
+			})
+		})
+
+		it("should preserve model dimension exactly in export/import roundtrip", async () => {
+			// This test specifically isolates the model dimension export/import roundtrip
+			// to catch the exact issue the user is experiencing
+
+			const testModelDimension = 768
+
+			// Step 1: Set up a provider without OpenAI Compatible settings in profile
+			const mockProviderProfiles = {
+				currentApiConfigName: "test-openai-compatible",
+				apiConfigs: {
+					"test-openai-compatible": {
+						apiProvider: "openai" as ProviderName,
+						id: "test-id",
+						// Remove OpenAI Compatible settings from provider profile
+					},
+				},
+				modeApiConfigs: {},
+			}
+
+			const mockGlobalSettings = {
+				mode: "code",
+				codebaseIndexConfig: {
+					codebaseIndexEnabled: true,
+					codebaseIndexEmbedderProvider: "openai-compatible" as const,
+					codebaseIndexEmbedderModelId: "custom-embedding-model",
+					codebaseIndexEmbedderBaseUrl: "https://api.example.com/v1",
+					codebaseIndexEmbedderModelDimension: testModelDimension,
+					// OpenAI Compatible settings are now stored directly in codebaseIndexConfig
+					codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1",
+					codebaseIndexOpenAiCompatibleModelDimension: testModelDimension,
+				},
+			}
+
+			// Step 2: Mock export operation
+			;(vscode.window.showSaveDialog as Mock).mockResolvedValue({
+				fsPath: "/mock/path/test-settings.json",
+			})
+
+			mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles)
+			mockContextProxy.export.mockResolvedValue(mockGlobalSettings)
+			;(fs.mkdir as Mock).mockResolvedValue(undefined)
+
+			// Step 3: Export settings
+			await exportSettings({
+				providerSettingsManager: mockProviderSettingsManager,
+				contextProxy: mockContextProxy,
+			})
+
+			// Step 4: Verify the exported data includes the model dimension
+			expect(safeWriteJson).toHaveBeenCalledWith("/mock/path/test-settings.json", {
+				providerProfiles: mockProviderProfiles,
+				globalSettings: mockGlobalSettings,
+			})
+
+			// Step 5: Get the exported data for import test
+			const exportedData = (safeWriteJson as Mock).mock.calls[0][1]
+			const exportedFileContent = JSON.stringify(exportedData)
+
+			// Step 6: Mock import operation
+			;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/test-settings.json" }])
+			;(fs.readFile as Mock).mockResolvedValue(exportedFileContent)
+
+			// Reset mocks for import
+			vi.clearAllMocks()
+			mockProviderSettingsManager.export.mockResolvedValue({
+				currentApiConfigName: "default",
+				apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } },
+			})
+			mockProviderSettingsManager.listConfig.mockResolvedValue([
+				{ name: "test-openai-compatible", id: "test-id", apiProvider: "openai" as ProviderName },
+			])
+
+			// Step 7: Import the settings back
+			const importResult = await importSettings({
+				providerSettingsManager: mockProviderSettingsManager,
+				contextProxy: mockContextProxy,
+				customModesManager: mockCustomModesManager,
+			})
+
+			// Step 8: Verify import was successful
+			expect(importResult.success).toBe(true)
+
+			// Step 9: Verify that the model dimension was preserved exactly in global settings
+			const importedGlobalSettings = mockContextProxy.setValues.mock.calls[0][0]
+			expect(importedGlobalSettings.codebaseIndexConfig?.codebaseIndexOpenAiCompatibleModelDimension).toBe(
+				testModelDimension,
+			)
+			expect(importedGlobalSettings.codebaseIndexConfig?.codebaseIndexOpenAiCompatibleBaseUrl).toBe(
+				"https://api.example.com/v1",
+			)
+
+			// Step 10: Verify that the embedder settings were imported correctly
+			expect(importedGlobalSettings.codebaseIndexConfig?.codebaseIndexEmbedderModelDimension).toBe(
+				testModelDimension,
+			)
+		})
+
+		it("should handle edge case model dimension values (0, null) correctly", async () => {
+			// Test with model dimension = 0 (which is falsy but valid)
+			const testModelDimension = 0
+
+			const mockProviderProfiles = {
+				currentApiConfigName: "test-openai-compatible",
+				apiConfigs: {
+					"test-openai-compatible": {
+						apiProvider: "openai" as ProviderName,
+						id: "test-id",
+						// Remove OpenAI Compatible settings from provider profile
+					},
+				},
+				modeApiConfigs: {},
+			}
+
+			const mockGlobalSettings = {
+				mode: "code",
+				codebaseIndexConfig: {
+					codebaseIndexEnabled: true,
+					codebaseIndexEmbedderProvider: "openai-compatible" as const,
+					codebaseIndexEmbedderModelId: "custom-embedding-model",
+					codebaseIndexEmbedderBaseUrl: "https://api.example.com/v1",
+					// OpenAI Compatible settings are now stored directly in codebaseIndexConfig
+					codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1",
+					codebaseIndexOpenAiCompatibleModelDimension: testModelDimension, // 0 is a valid value
+				},
+			}
+
+			// Mock export operation
+			;(vscode.window.showSaveDialog as Mock).mockResolvedValue({
+				fsPath: "/mock/path/test-settings.json",
+			})
+
+			mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles)
+			mockContextProxy.export.mockResolvedValue(mockGlobalSettings)
+			;(fs.mkdir as Mock).mockResolvedValue(undefined)
+
+			// Export settings
+			await exportSettings({
+				providerSettingsManager: mockProviderSettingsManager,
+				contextProxy: mockContextProxy,
+			})
+
+			// Verify the exported data includes the model dimension even when it's 0
+			const exportedData = (safeWriteJson as Mock).mock.calls[0][1]
+			expect(exportedData.globalSettings.codebaseIndexConfig.codebaseIndexOpenAiCompatibleModelDimension).toBe(0)
+
+			// Test import roundtrip
+			const exportedFileContent = JSON.stringify(exportedData)
+			;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/test-settings.json" }])
+			;(fs.readFile as Mock).mockResolvedValue(exportedFileContent)
+
+			// Reset mocks for import
+			vi.clearAllMocks()
+			mockProviderSettingsManager.export.mockResolvedValue({
+				currentApiConfigName: "default",
+				apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } },
+			})
+			mockProviderSettingsManager.listConfig.mockResolvedValue([
+				{ name: "test-openai-compatible", id: "test-id", apiProvider: "openai" as ProviderName },
+			])
+
+			// Import the settings back
+			const importResult = await importSettings({
+				providerSettingsManager: mockProviderSettingsManager,
+				contextProxy: mockContextProxy,
+				customModesManager: mockCustomModesManager,
+			})
+
+			expect(importResult.success).toBe(true)
+
+			// Verify that model dimension 0 was preserved in global settings
+			const setValuesCall = mockContextProxy.setValues.mock.calls[0][0]
+			expect(setValuesCall.codebaseIndexConfig?.codebaseIndexOpenAiCompatibleModelDimension).toBe(0)
+		})
+
+		it("should handle missing model dimension gracefully", async () => {
+			// Test when model dimension is undefined in global state
+			const mockProviderProfiles = {
+				currentApiConfigName: "test-openai-compatible",
+				apiConfigs: {
+					"test-openai-compatible": {
+						apiProvider: "openai" as ProviderName,
+						id: "test-id",
+						// Remove OpenAI Compatible settings from provider profile
+					},
+				},
+				modeApiConfigs: {},
+			}
+
+			const mockGlobalSettings = {
+				mode: "code",
+				codebaseIndexConfig: {
+					codebaseIndexEnabled: true,
+					codebaseIndexEmbedderProvider: "openai-compatible" as const,
+					codebaseIndexEmbedderModelId: "custom-embedding-model",
+					codebaseIndexEmbedderBaseUrl: "https://api.example.com/v1",
+				},
+			}
+
+			// Mock getGlobalState to return undefined for model dimension
+			mockContextProxy.getGlobalState = vi.fn().mockImplementation((key: string) => {
+				if (key === "codebaseIndexOpenAiCompatibleBaseUrl") {
+					return "https://api.example.com/v1"
+				}
+				if (key === "codebaseIndexOpenAiCompatibleModelDimension") {
+					return undefined
+				}
+				return undefined
+			})
+
+			// Mock export operation
+			;(vscode.window.showSaveDialog as Mock).mockResolvedValue({
+				fsPath: "/mock/path/test-settings.json",
+			})
+
+			mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles)
+			mockContextProxy.export.mockResolvedValue(mockGlobalSettings)
+			;(fs.mkdir as Mock).mockResolvedValue(undefined)
+
+			// Export settings
+			await exportSettings({
+				providerSettingsManager: mockProviderSettingsManager,
+				contextProxy: mockContextProxy,
+			})
+
+			// Verify the exported data does NOT include model dimension when it's undefined
+			const exportedData = (safeWriteJson as Mock).mock.calls[0][1]
+			expect(exportedData.globalSettings.codebaseIndexConfig.codebaseIndexEmbedderModelDimension).toBeUndefined()
+		})
+
+		it("should handle provider mismatch during import - BUG REPRODUCTION", async () => {
+			// This test reproduces the bug where model dimension is lost when importing
+			// settings where the current provider is different from the exported provider
+
+			// Step 1: Create exported settings from "provider-a" with model dimension
+			const exportedSettings = {
+				providerProfiles: {
+					currentApiConfigName: "provider-a",
+					apiConfigs: {
+						"provider-a": {
+							apiProvider: "openai" as ProviderName,
+							id: "provider-a-id",
+							codebaseIndexOpenAiCompatibleBaseUrl: "https://api-a.example.com/v1",
+							codebaseIndexOpenAiCompatibleModelDimension: 1536,
+						},
+						"provider-b": {
+							apiProvider: "anthropic" as ProviderName,
+							id: "provider-b-id",
+						},
+					},
+					modeApiConfigs: {},
+				},
+				globalSettings: {
+					mode: "code",
+					codebaseIndexConfig: {
+						codebaseIndexEnabled: true,
+						codebaseIndexEmbedderProvider: "openai-compatible" as const,
+						codebaseIndexEmbedderModelId: "text-embedding-3-small",
+						codebaseIndexEmbedderBaseUrl: "https://api-a.example.com/v1",
+						codebaseIndexEmbedderModelDimension: 1536,
+					},
+				},
+			}
+
+			// Step 2: Set up import environment where current provider is "provider-b" (different!)
+			const currentProviderProfiles = {
+				currentApiConfigName: "provider-b", // Different from exported settings!
+				apiConfigs: {
+					"provider-b": {
+						apiProvider: "anthropic" as ProviderName,
+						id: "provider-b-id",
+					},
+				},
+			}
+
+			// Step 3: Mock import operation
+			;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
+			;(fs.readFile as Mock).mockResolvedValue(JSON.stringify(exportedSettings))
+
+			mockProviderSettingsManager.export.mockResolvedValue(currentProviderProfiles)
+			mockProviderSettingsManager.listConfig.mockResolvedValue([
+				{ name: "provider-a", id: "provider-a-id", apiProvider: "openai" as ProviderName },
+				{ name: "provider-b", id: "provider-b-id", apiProvider: "anthropic" as ProviderName },
+			])
+
+			// Step 4: Import the settings
+			const importResult = await importSettings({
+				providerSettingsManager: mockProviderSettingsManager,
+				contextProxy: mockContextProxy,
+				customModesManager: mockCustomModesManager,
+			})
+
+			expect(importResult.success).toBe(true)
+
+			// Step 5: Check what was imported
+			const importedProviderProfiles = mockProviderSettingsManager.import.mock.calls[0][0]
+
+			// The bug: provider-a should have its model dimension preserved, but it might be lost
+			// because the import logic only updates the CURRENT provider (provider-b)
+			const providerA = importedProviderProfiles.apiConfigs["provider-a"]
+			const providerB = importedProviderProfiles.apiConfigs["provider-b"]
+
+			// This should pass but might fail due to the bug
+			expect(providerA.codebaseIndexOpenAiCompatibleModelDimension).toBe(1536)
+			expect(providerA.codebaseIndexOpenAiCompatibleBaseUrl).toBe("https://api-a.example.com/v1")
+
+			// Provider B should not have OpenAI Compatible settings
+			expect(providerB.codebaseIndexOpenAiCompatibleModelDimension).toBeUndefined()
+			expect(providerB.codebaseIndexOpenAiCompatibleBaseUrl).toBeUndefined()
+		})
+
+		it("should NOT copy OpenAI Compatible settings to provider profiles - FIXED BEHAVIOR", async () => {
+			// This test verifies the FIXED behavior: OpenAI Compatible settings stay in global settings only
+
+			const exportedSettings = {
+				providerProfiles: {
+					currentApiConfigName: "openai-compatible-provider",
+					apiConfigs: {
+						"openai-compatible-provider": {
+							apiProvider: "openai" as ProviderName,
+							id: "openai-compatible-id",
+							// NO OpenAI Compatible settings here in the fixed version
+						},
+						"anthropic-provider": {
+							apiProvider: "anthropic" as ProviderName,
+							id: "anthropic-id",
+						},
+					},
+					modeApiConfigs: {},
+				},
+				globalSettings: {
+					mode: "code",
+					codebaseIndexConfig: {
+						codebaseIndexEnabled: true,
+						codebaseIndexEmbedderProvider: "openai-compatible" as const,
+						codebaseIndexEmbedderModelId: "text-embedding-3-small",
+						codebaseIndexEmbedderBaseUrl: "https://new-url.example.com/v1",
+						codebaseIndexEmbedderModelDimension: 1536,
+						// OpenAI Compatible settings are stored here
+						codebaseIndexOpenAiCompatibleBaseUrl: "https://new-url.example.com/v1",
+						codebaseIndexOpenAiCompatibleModelDimension: 1536,
+					},
+				},
+			}
+
+			const currentProviderProfiles = {
+				currentApiConfigName: "anthropic-provider",
+				apiConfigs: {
+					"anthropic-provider": {
+						apiProvider: "anthropic" as ProviderName,
+						id: "anthropic-id",
+					},
+				},
+			}
+
+			;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
+			;(fs.readFile as Mock).mockResolvedValue(JSON.stringify(exportedSettings))
+
+			mockProviderSettingsManager.export.mockResolvedValue(currentProviderProfiles)
+			mockProviderSettingsManager.listConfig.mockResolvedValue([
+				{
+					name: "openai-compatible-provider",
+					id: "openai-compatible-id",
+					apiProvider: "openai" as ProviderName,
+				},
+				{ name: "anthropic-provider", id: "anthropic-id", apiProvider: "anthropic" as ProviderName },
+			])
+
+			const importResult = await importSettings({
+				providerSettingsManager: mockProviderSettingsManager,
+				contextProxy: mockContextProxy,
+				customModesManager: mockCustomModesManager,
+			})
+
+			expect(importResult.success).toBe(true)
+
+			// Verify OpenAI Compatible settings are imported to global settings
+			const importedGlobalSettings = mockContextProxy.setValues.mock.calls[0][0]
+			expect(importedGlobalSettings.codebaseIndexConfig?.codebaseIndexOpenAiCompatibleBaseUrl).toBe(
+				"https://new-url.example.com/v1",
+			)
+			expect(importedGlobalSettings.codebaseIndexConfig?.codebaseIndexOpenAiCompatibleModelDimension).toBe(1536)
+
+			// Verify provider profiles do NOT have OpenAI Compatible settings
+			const importedProviderProfiles = mockProviderSettingsManager.import.mock.calls[0][0]
+			const openaiCompatibleProvider = importedProviderProfiles.apiConfigs["openai-compatible-provider"]
+			const anthropicProvider = importedProviderProfiles.apiConfigs["anthropic-provider"]
+
+			// Neither provider should have OpenAI Compatible settings
+			expect(openaiCompatibleProvider.codebaseIndexOpenAiCompatibleBaseUrl).toBeUndefined()
+			expect(openaiCompatibleProvider.codebaseIndexOpenAiCompatibleModelDimension).toBeUndefined()
+			expect(anthropicProvider.codebaseIndexOpenAiCompatibleBaseUrl).toBeUndefined()
+			expect(anthropicProvider.codebaseIndexOpenAiCompatibleModelDimension).toBeUndefined()
+		})
+
+		it("should keep OpenAI Compatible settings in global state only - FIXED BEHAVIOR", async () => {
+			// This test verifies that OpenAI Compatible settings remain in global state
+			// and are NOT copied to provider profiles
+
+			const exportedSettings = {
+				providerProfiles: {
+					currentApiConfigName: "anthropic-provider",
+					apiConfigs: {
+						"anthropic-provider": {
+							apiProvider: "anthropic" as ProviderName,
+							id: "anthropic-id",
+						},
+						"openai-compatible-provider": {
+							apiProvider: "openai" as ProviderName,
+							id: "openai-compatible-id",
+							// NO OpenAI Compatible settings in provider profiles
+						},
+					},
+					modeApiConfigs: {},
+				},
+				globalSettings: {
+					mode: "code",
+					codebaseIndexConfig: {
+						codebaseIndexEnabled: true,
+						codebaseIndexEmbedderProvider: "openai-compatible" as const,
+						codebaseIndexEmbedderModelId: "text-embedding-3-small",
+						codebaseIndexEmbedderBaseUrl: "https://updated.example.com/v1",
+						codebaseIndexEmbedderModelDimension: 1536,
+						// OpenAI Compatible settings are stored here
+						codebaseIndexOpenAiCompatibleBaseUrl: "https://updated.example.com/v1",
+						codebaseIndexOpenAiCompatibleModelDimension: 1536,
+					},
+				},
+			}
+
+			const currentProviderProfiles = {
+				currentApiConfigName: "default",
+				apiConfigs: {
+					default: {
+						apiProvider: "openai" as ProviderName,
+						id: "default-id",
+					},
+				},
+			}
+
+			;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
+			;(fs.readFile as Mock).mockResolvedValue(JSON.stringify(exportedSettings))
+
+			mockProviderSettingsManager.export.mockResolvedValue(currentProviderProfiles)
+			mockProviderSettingsManager.listConfig.mockResolvedValue([
+				{ name: "anthropic-provider", id: "anthropic-id", apiProvider: "anthropic" as ProviderName },
+				{
+					name: "openai-compatible-provider",
+					id: "openai-compatible-id",
+					apiProvider: "openai" as ProviderName,
+				},
+				{ name: "default", id: "default-id", apiProvider: "openai" as ProviderName },
+			])
+
+			const importResult = await importSettings({
+				providerSettingsManager: mockProviderSettingsManager,
+				contextProxy: mockContextProxy,
+				customModesManager: mockCustomModesManager,
+			})
+
+			expect(importResult.success).toBe(true)
+
+			// Verify OpenAI Compatible settings are imported to global settings
+			const importedGlobalSettings = mockContextProxy.setValues.mock.calls[0][0]
+			expect(importedGlobalSettings.codebaseIndexConfig?.codebaseIndexOpenAiCompatibleBaseUrl).toBe(
+				"https://updated.example.com/v1",
+			)
+			expect(importedGlobalSettings.codebaseIndexConfig?.codebaseIndexOpenAiCompatibleModelDimension).toBe(1536)
+
+			// Verify NO provider profiles have OpenAI Compatible settings
+			const importedProviderProfiles = mockProviderSettingsManager.import.mock.calls[0][0]
+			const anthropicProvider = importedProviderProfiles.apiConfigs["anthropic-provider"]
+			const openaiCompatibleProvider = importedProviderProfiles.apiConfigs["openai-compatible-provider"]
+
+			// Neither provider should have OpenAI Compatible settings
+			expect(anthropicProvider.codebaseIndexOpenAiCompatibleBaseUrl).toBeUndefined()
+			expect(anthropicProvider.codebaseIndexOpenAiCompatibleModelDimension).toBeUndefined()
+			expect(openaiCompatibleProvider.codebaseIndexOpenAiCompatibleBaseUrl).toBeUndefined()
+			expect(openaiCompatibleProvider.codebaseIndexOpenAiCompatibleModelDimension).toBeUndefined()
+		})
+
+		it("should export OpenAI Compatible settings from global state when provider is openai-compatible", async () => {
+			// This test reproduces the bug where codebaseIndexEmbedderModelDimension is missing from exported JSON
+			// when the OpenAI Compatible settings are stored in global state via contextProxy
+
+			;(vscode.window.showSaveDialog as Mock).mockResolvedValue({
+				fsPath: "/mock/path/roo-code-settings.json",
+			})
+
+			// Set up provider profiles - note that the OpenAI Compatible provider does NOT have
+			// the codebaseIndexOpenAiCompatibleBaseUrl and codebaseIndexOpenAiCompatibleModelDimension
+			// fields in the provider profile itself
+			const mockProviderProfiles = {
+				currentApiConfigName: "openrouter-provider", // Current provider is OpenRouter
+				apiConfigs: {
+					"openrouter-provider": {
+						apiProvider: "openrouter" as ProviderName,
+						id: "openrouter-id",
+						// OpenRouter doesn't have OpenAI Compatible fields
+					},
+				},
+				modeApiConfigs: {},
+			}
+
+			// The global settings now include OpenAI Compatible settings directly in codebaseIndexConfig
+			const mockGlobalSettings = {
+				mode: "code",
+				codebaseIndexConfig: {
+					codebaseIndexEnabled: true,
+					codebaseIndexEmbedderProvider: "openai-compatible" as const,
+					codebaseIndexEmbedderModelId: "text-embedding-3-small",
+					codebaseIndexEmbedderBaseUrl: "https://custom-api.example.com/v1",
+					codebaseIndexEmbedderModelDimension: 1536,
+					// OpenAI Compatible settings are now included directly
+					codebaseIndexOpenAiCompatibleBaseUrl: "https://custom-api.example.com/v1",
+					codebaseIndexOpenAiCompatibleModelDimension: 1536,
+				},
+			}
+
+			mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles)
+			mockContextProxy.export.mockResolvedValue(mockGlobalSettings)
+			;(fs.mkdir as Mock).mockResolvedValue(undefined)
+
+			await exportSettings({
+				providerSettingsManager: mockProviderSettingsManager,
+				contextProxy: mockContextProxy,
+			})
+
+			// Verify that the exported JSON contains the OpenAI Compatible settings
+			const exportedData = (safeWriteJson as Mock).mock.calls[0][1]
+
+			// With the fix, these values are now properly exported
+			expect(exportedData.globalSettings.codebaseIndexConfig.codebaseIndexOpenAiCompatibleModelDimension).toBe(
+				1536,
+			)
+			expect(exportedData.globalSettings.codebaseIndexConfig.codebaseIndexOpenAiCompatibleBaseUrl).toBe(
+				"https://custom-api.example.com/v1",
+			)
+		})
 	})
 })

+ 10 - 1
src/core/config/importExport.ts

@@ -68,6 +68,9 @@ export async function importSettingsFromPath(
 			(globalSettings.customModes ?? []).map((mode) => customModesManager.updateCustomMode(mode.slug, mode)),
 		)
 
+		// OpenAI Compatible settings are now correctly stored in codebaseIndexConfig
+		// They will be imported automatically with the config - no special handling needed
+
 		await providerSettingsManager.import(providerProfiles)
 		await contextProxy.setValues(globalSettings)
 
@@ -161,10 +164,16 @@ export const exportSettings = async ({ providerSettingsManager, contextProxy }:
 			return
 		}
 
+		// OpenAI Compatible settings are now correctly stored in codebaseIndexConfig
+		// No workaround needed - they will be exported automatically with the config
+
 		const dirname = path.dirname(uri.fsPath)
 		await fs.mkdir(dirname, { recursive: true })
 		await safeWriteJson(uri.fsPath, { providerProfiles, globalSettings })
-	} catch (e) {}
+	} catch (e) {
+		console.error("Failed to export settings:", e)
+		// Don't re-throw - the UI will handle showing error messages
+	}
 }
 
 /**

+ 24 - 23
src/services/code-index/__tests__/config-manager.spec.ts

@@ -96,10 +96,10 @@ describe("CodeIndexConfigManager", () => {
 				codebaseIndexEmbedderProvider: "openai-compatible",
 				codebaseIndexEmbedderBaseUrl: "",
 				codebaseIndexEmbedderModelId: "text-embedding-3-large",
+				codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1",
 			}
 			mockContextProxy.getGlobalState.mockImplementation((key: string) => {
 				if (key === "codebaseIndexConfig") return mockGlobalState
-				if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
 				return undefined
 			})
 
@@ -134,11 +134,11 @@ describe("CodeIndexConfigManager", () => {
 				codebaseIndexEmbedderProvider: "openai-compatible",
 				codebaseIndexEmbedderBaseUrl: "",
 				codebaseIndexEmbedderModelId: "custom-model",
+				codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1",
+				codebaseIndexOpenAiCompatibleModelDimension: 1024,
 			}
 			mockContextProxy.getGlobalState.mockImplementation((key: string) => {
 				if (key === "codebaseIndexConfig") return mockGlobalState
-				if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
-				if (key === "codebaseIndexOpenAiCompatibleModelDimension") return 1024
 				return undefined
 			})
 			setupSecretMocks({
@@ -173,11 +173,11 @@ describe("CodeIndexConfigManager", () => {
 				codebaseIndexEmbedderProvider: "openai-compatible",
 				codebaseIndexEmbedderBaseUrl: "",
 				codebaseIndexEmbedderModelId: "custom-model",
+				codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1",
+				// modelDimension is not set
 			}
 			mockContextProxy.getGlobalState.mockImplementation((key: string) => {
 				if (key === "codebaseIndexConfig") return mockGlobalState
-				if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
-				if (key === "codebaseIndexOpenAiCompatibleModelDimension") return undefined
 				return undefined
 			})
 			setupSecretMocks({
@@ -197,6 +197,7 @@ describe("CodeIndexConfigManager", () => {
 				openAiCompatibleOptions: {
 					baseUrl: "https://api.example.com/v1",
 					apiKey: "test-openai-compatible-key",
+					// modelDimension is undefined when not set
 				},
 				qdrantUrl: "http://qdrant.local",
 				qdrantApiKey: "test-qdrant-key",
@@ -211,11 +212,11 @@ describe("CodeIndexConfigManager", () => {
 				codebaseIndexEmbedderProvider: "openai-compatible",
 				codebaseIndexEmbedderBaseUrl: "",
 				codebaseIndexEmbedderModelId: "custom-model",
+				codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1",
+				codebaseIndexOpenAiCompatibleModelDimension: "invalid-dimension", // Invalid type
 			}
 			mockContextProxy.getGlobalState.mockImplementation((key: string) => {
 				if (key === "codebaseIndexConfig") return mockGlobalState
-				if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
-				if (key === "codebaseIndexOpenAiCompatibleModelDimension") return "invalid-dimension"
 				return undefined
 			})
 			setupSecretMocks({
@@ -461,9 +462,9 @@ describe("CodeIndexConfigManager", () => {
 							codebaseIndexQdrantUrl: "http://qdrant.local",
 							codebaseIndexEmbedderProvider: "openai-compatible",
 							codebaseIndexEmbedderModelId: "text-embedding-3-small",
+							codebaseIndexOpenAiCompatibleBaseUrl: "https://old-api.example.com/v1",
 						}
 					}
-					if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://old-api.example.com/v1"
 					return undefined
 				})
 				setupSecretMocks({
@@ -481,9 +482,9 @@ describe("CodeIndexConfigManager", () => {
 							codebaseIndexQdrantUrl: "http://qdrant.local",
 							codebaseIndexEmbedderProvider: "openai-compatible",
 							codebaseIndexEmbedderModelId: "text-embedding-3-small",
+							codebaseIndexOpenAiCompatibleBaseUrl: "https://new-api.example.com/v1",
 						}
 					}
-					if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://new-api.example.com/v1"
 					return undefined
 				})
 
@@ -500,9 +501,9 @@ describe("CodeIndexConfigManager", () => {
 							codebaseIndexQdrantUrl: "http://qdrant.local",
 							codebaseIndexEmbedderProvider: "openai-compatible",
 							codebaseIndexEmbedderModelId: "text-embedding-3-small",
+							codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1",
 						}
 					}
-					if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
 					return undefined
 				})
 				setupSecretMocks({
@@ -531,10 +532,10 @@ describe("CodeIndexConfigManager", () => {
 							codebaseIndexQdrantUrl: "http://qdrant.local",
 							codebaseIndexEmbedderProvider: "openai-compatible",
 							codebaseIndexEmbedderModelId: "custom-model",
+							codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1",
+							codebaseIndexOpenAiCompatibleModelDimension: 1024,
 						}
 					}
-					if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
-					if (key === "codebaseIndexOpenAiCompatibleModelDimension") return 1024
 					return undefined
 				})
 				setupSecretMocks({
@@ -552,10 +553,10 @@ describe("CodeIndexConfigManager", () => {
 							codebaseIndexQdrantUrl: "http://qdrant.local",
 							codebaseIndexEmbedderProvider: "openai-compatible",
 							codebaseIndexEmbedderModelId: "custom-model",
+							codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1",
+							codebaseIndexOpenAiCompatibleModelDimension: 2048,
 						}
 					}
-					if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
-					if (key === "codebaseIndexOpenAiCompatibleModelDimension") return 2048
 					return undefined
 				})
 
@@ -614,10 +615,10 @@ describe("CodeIndexConfigManager", () => {
 							codebaseIndexQdrantUrl: "http://qdrant.local",
 							codebaseIndexEmbedderProvider: "openai-compatible",
 							codebaseIndexEmbedderModelId: "custom-model",
+							codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1",
+							// modelDimension not set initially
 						}
 					}
-					if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
-					if (key === "codebaseIndexOpenAiCompatibleModelDimension") return undefined
 					return undefined
 				})
 				setupSecretMocks({
@@ -635,10 +636,10 @@ describe("CodeIndexConfigManager", () => {
 							codebaseIndexQdrantUrl: "http://qdrant.local",
 							codebaseIndexEmbedderProvider: "openai-compatible",
 							codebaseIndexEmbedderModelId: "custom-model",
+							codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1",
+							codebaseIndexOpenAiCompatibleModelDimension: 1024,
 						}
 					}
-					if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
-					if (key === "codebaseIndexOpenAiCompatibleModelDimension") return 1024
 					return undefined
 				})
 
@@ -655,10 +656,10 @@ describe("CodeIndexConfigManager", () => {
 							codebaseIndexQdrantUrl: "http://qdrant.local",
 							codebaseIndexEmbedderProvider: "openai-compatible",
 							codebaseIndexEmbedderModelId: "custom-model",
+							codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1",
+							codebaseIndexOpenAiCompatibleModelDimension: 1024,
 						}
 					}
-					if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
-					if (key === "codebaseIndexOpenAiCompatibleModelDimension") return 1024
 					return undefined
 				})
 				setupSecretMocks({
@@ -676,10 +677,10 @@ describe("CodeIndexConfigManager", () => {
 							codebaseIndexQdrantUrl: "http://qdrant.local",
 							codebaseIndexEmbedderProvider: "openai-compatible",
 							codebaseIndexEmbedderModelId: "custom-model",
+							codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1",
+							// modelDimension removed
 						}
 					}
-					if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
-					if (key === "codebaseIndexOpenAiCompatibleModelDimension") return undefined
 					return undefined
 				})
 
@@ -1082,9 +1083,9 @@ describe("CodeIndexConfigManager", () => {
 						codebaseIndexEnabled: true,
 						codebaseIndexQdrantUrl: "http://qdrant.local",
 						codebaseIndexEmbedderProvider: "openai-compatible",
+						codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1",
 					}
 				}
-				if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
 				return undefined
 			})
 			setupSecretMocks({

+ 5 - 4
src/services/code-index/config-manager.ts

@@ -62,11 +62,12 @@ export class CodeIndexConfigManager {
 
 		const openAiKey = this.contextProxy?.getSecret("codeIndexOpenAiKey") ?? ""
 		const qdrantApiKey = this.contextProxy?.getSecret("codeIndexQdrantApiKey") ?? ""
-		const openAiCompatibleBaseUrl = this.contextProxy?.getGlobalState("codebaseIndexOpenAiCompatibleBaseUrl") ?? ""
+		// Fix: Read OpenAI Compatible settings from the correct location within codebaseIndexConfig
+		const openAiCompatibleBaseUrl = codebaseIndexConfig.codebaseIndexOpenAiCompatibleBaseUrl ?? ""
 		const openAiCompatibleApiKey = this.contextProxy?.getSecret("codebaseIndexOpenAiCompatibleApiKey") ?? ""
-		const openAiCompatibleModelDimension = this.contextProxy?.getGlobalState(
-			"codebaseIndexOpenAiCompatibleModelDimension",
-		) as number | undefined
+		const openAiCompatibleModelDimension = codebaseIndexConfig.codebaseIndexOpenAiCompatibleModelDimension as
+			| number
+			| undefined
 		const geminiApiKey = this.contextProxy?.getSecret("codebaseIndexGeminiApiKey") ?? ""
 
 		// Update instance variables with configuration

+ 3 - 2
webview-ui/src/components/chat/CodeIndexPopover.tsx

@@ -121,9 +121,10 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
 					codebaseIndexConfig.codebaseIndexSearchMinScore ?? CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_MIN_SCORE,
 				codeIndexOpenAiKey: "",
 				codeIndexQdrantApiKey: "",
-				codebaseIndexOpenAiCompatibleBaseUrl: "",
+				codebaseIndexOpenAiCompatibleBaseUrl: codebaseIndexConfig.codebaseIndexOpenAiCompatibleBaseUrl || "",
 				codebaseIndexOpenAiCompatibleApiKey: "",
-				codebaseIndexOpenAiCompatibleModelDimension: undefined,
+				codebaseIndexOpenAiCompatibleModelDimension:
+					codebaseIndexConfig.codebaseIndexOpenAiCompatibleModelDimension || undefined,
 				codebaseIndexGeminiApiKey: "",
 			}
 			setInitialSettings(settings)