diff --git a/packages/types/src/codebase-index.ts b/packages/types/src/codebase-index.ts index 51a10f0154..0ad19d8676 100644 --- a/packages/types/src/codebase-index.ts +++ b/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 diff --git a/src/core/config/__tests__/importExport.spec.ts b/src/core/config/__tests__/importExport.spec.ts index 1f6bd5f28e..052bfc77f8 100644 --- a/src/core/config/__tests__/importExport.spec.ts +++ b/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", + ) + }) }) }) diff --git a/src/core/config/importExport.ts b/src/core/config/importExport.ts index 6ffbbf7136..c3d6f9c215 100644 --- a/src/core/config/importExport.ts +++ b/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 + } } /** diff --git a/src/services/code-index/__tests__/config-manager.spec.ts b/src/services/code-index/__tests__/config-manager.spec.ts index 29c4f979dd..994f228e52 100644 --- a/src/services/code-index/__tests__/config-manager.spec.ts +++ b/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({ diff --git a/src/services/code-index/config-manager.ts b/src/services/code-index/config-manager.ts index 4797f95d01..26ea0e2a48 100644 --- a/src/services/code-index/config-manager.ts +++ b/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 diff --git a/webview-ui/src/components/chat/CodeIndexPopover.tsx b/webview-ui/src/components/chat/CodeIndexPopover.tsx index 22b4d28192..6c9ea1c204 100644 --- a/webview-ui/src/components/chat/CodeIndexPopover.tsx +++ b/webview-ui/src/components/chat/CodeIndexPopover.tsx @@ -121,9 +121,10 @@ export const CodeIndexPopover: React.FC = ({ 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)