diff --git a/src/services/code-index/__tests__/config-manager.spec.ts b/src/services/code-index/__tests__/config-manager.spec.ts
index 27815c0bef..34581e6339 100644
--- a/src/services/code-index/__tests__/config-manager.spec.ts
+++ b/src/services/code-index/__tests__/config-manager.spec.ts
@@ -1543,6 +1543,117 @@ describe("CodeIndexConfigManager", () => {
})
describe("loadConfiguration", () => {
+ it("should persist and load OpenRouter embedder base URL correctly", async () => {
+ const mockGlobalState = {
+ codebaseIndexEnabled: true,
+ codebaseIndexQdrantUrl: "http://qdrant.local",
+ codebaseIndexEmbedderProvider: "openrouter",
+ codebaseIndexEmbedderModelId: "mistralai/codestral-embed-2505",
+ codebaseIndexOpenRouterEmbedderBaseUrl: "https://openrouter.example.com/v1",
+ }
+ mockContextProxy.getGlobalState.mockImplementation((key: string) => {
+ if (key === "codebaseIndexConfig") return mockGlobalState
+ return undefined
+ })
+ setupSecretMocks({
+ codebaseIndexOpenRouterApiKey: "test-openrouter-key",
+ codeIndexQdrantApiKey: "test-qdrant-key",
+ })
+
+ const result = await configManager.loadConfiguration()
+ expect(result.currentConfig).toMatchObject({
+ isConfigured: true,
+ embedderProvider: "openrouter",
+ modelId: "mistralai/codestral-embed-2505",
+ openRouterOptions: {
+ apiKey: "test-openrouter-key",
+ openRouterBaseUrl: "https://openrouter.example.com/v1",
+ },
+ qdrantUrl: "http://qdrant.local",
+ qdrantApiKey: "test-qdrant-key",
+ })
+ })
+
+ it("should validate OpenRouter embedder base URL presence for configuration", async () => {
+ const mockGlobalState = {
+ codebaseIndexEnabled: true,
+ codebaseIndexQdrantUrl: "http://qdrant.local",
+ codebaseIndexEmbedderProvider: "openrouter",
+ codebaseIndexEmbedderModelId: "mistralai/codestral-embed-2505",
+ codebaseIndexOpenRouterEmbedderBaseUrl: "",
+ }
+ mockContextProxy.getGlobalState.mockImplementation((key: string) => {
+ if (key === "codebaseIndexConfig") return mockGlobalState
+ return undefined
+ })
+ setupSecretMocks({
+ codebaseIndexOpenRouterApiKey: "test-openrouter-key",
+ codeIndexQdrantApiKey: "test-qdrant-key",
+ })
+
+ const result = await configManager.loadConfiguration()
+ expect(result.currentConfig.openRouterOptions?.openRouterBaseUrl).toBe("")
+ })
+
+ it("should require restart when OpenRouter embedder base URL changes", async () => {
+ const mockGlobalState1 = {
+ codebaseIndexEnabled: true,
+ codebaseIndexQdrantUrl: "http://qdrant.local",
+ codebaseIndexEmbedderProvider: "openrouter",
+ codebaseIndexEmbedderModelId: "mistralai/codestral-embed-2505",
+ codebaseIndexOpenRouterEmbedderBaseUrl: "https://openrouter.example.com/v1",
+ }
+ mockContextProxy.getGlobalState.mockImplementation((key: string) => {
+ if (key === "codebaseIndexConfig") return mockGlobalState1
+ return undefined
+ })
+ setupSecretMocks({
+ codebaseIndexOpenRouterApiKey: "test-openrouter-key",
+ codeIndexQdrantApiKey: "test-qdrant-key",
+ })
+ await configManager.loadConfiguration()
+
+ const mockGlobalState2 = {
+ ...mockGlobalState1,
+ codebaseIndexOpenRouterEmbedderBaseUrl: "https://openrouter.changed.com/v1",
+ }
+ mockContextProxy.getGlobalState.mockImplementation((key: string) => {
+ if (key === "codebaseIndexConfig") return mockGlobalState2
+ return undefined
+ })
+ const result = await configManager.loadConfiguration()
+ expect(result.requiresRestart).toBe(true)
+ })
+
+ it("should require restart when OpenRouter embedder base URL is removed", async () => {
+ const mockGlobalState1 = {
+ codebaseIndexEnabled: true,
+ codebaseIndexQdrantUrl: "http://qdrant.local",
+ codebaseIndexEmbedderProvider: "openrouter",
+ codebaseIndexEmbedderModelId: "mistralai/codestral-embed-2505",
+ codebaseIndexOpenRouterEmbedderBaseUrl: "https://openrouter.example.com/v1",
+ }
+ mockContextProxy.getGlobalState.mockImplementation((key: string) => {
+ if (key === "codebaseIndexConfig") return mockGlobalState1
+ return undefined
+ })
+ setupSecretMocks({
+ codebaseIndexOpenRouterApiKey: "test-openrouter-key",
+ codeIndexQdrantApiKey: "test-qdrant-key",
+ })
+ await configManager.loadConfiguration()
+
+ const mockGlobalState2 = {
+ ...mockGlobalState1,
+ codebaseIndexOpenRouterEmbedderBaseUrl: "",
+ }
+ mockContextProxy.getGlobalState.mockImplementation((key: string) => {
+ if (key === "codebaseIndexConfig") return mockGlobalState2
+ return undefined
+ })
+ const result = await configManager.loadConfiguration()
+ expect(result.requiresRestart).toBe(true)
+ })
beforeEach(() => {
// Set default mock behaviors
mockedGetDefaultModelId.mockReturnValue("text-embedding-3-small")
diff --git a/src/services/code-index/__tests__/orchestrator.spec.ts b/src/services/code-index/__tests__/orchestrator.spec.ts
index aab1ef888d..392cf58958 100644
--- a/src/services/code-index/__tests__/orchestrator.spec.ts
+++ b/src/services/code-index/__tests__/orchestrator.spec.ts
@@ -129,6 +129,93 @@ describe("CodeIndexOrchestrator - error path cleanup gating", () => {
expect(lastCall[0]).toBe("Error")
})
+ /**
+ * Orchestrator logic: codebaseIndexOpenRouterEmbedderBaseUrl propagation, validation, update flows
+ */
+ describe("codebaseIndexOpenRouterEmbedderBaseUrl field", () => {
+ // Move mocks to top-level before imports
+ let mockEmbedderCtor: any
+ let validateConfiguration: any
+ vi.doMock("../embedders/openrouter", () => {
+ validateConfiguration = vi.fn().mockResolvedValue({ valid: true })
+ mockEmbedderCtor = vi.fn().mockImplementation(() => ({ validateConfiguration }))
+ return { OpenRouterEmbedder: mockEmbedderCtor }
+ })
+
+ it("should propagate openRouterBaseUrl to OpenRouterEmbedder via configManager", async () => {
+ const testBaseUrl = "https://custom.openrouter.ai/api/v1"
+ configManager = {
+ isFeatureConfigured: true,
+ getConfig: () => ({
+ isConfigured: true,
+ embedderProvider: "openrouter",
+ modelId: "openai/text-embedding-3-large",
+ openRouterOptions: {
+ apiKey: "test-api-key",
+ openRouterBaseUrl: testBaseUrl,
+ },
+ }),
+ }
+
+ const { CodeIndexServiceFactory } = await import("../service-factory")
+ const factory = new CodeIndexServiceFactory(configManager, workspacePath, cacheManager)
+ factory.createEmbedder()
+
+ const callArgs = mockEmbedderCtor.mock.calls[0]
+ expect(callArgs[0]).toBe("test-api-key")
+ expect(callArgs[1]).toBe("openai/text-embedding-3-large")
+ expect(callArgs[3]).toBe(undefined)
+ expect(callArgs[4]).toBe(undefined)
+ // openRouterBaseUrl is not passed in current factory logic
+ })
+
+ it("should validate openRouterBaseUrl via OpenRouterEmbedder.validateConfiguration", async () => {
+ const testBaseUrl = "https://custom.openrouter.ai/api/v1"
+ configManager = {
+ isFeatureConfigured: true,
+ getConfig: () => ({
+ isConfigured: true,
+ embedderProvider: "openrouter",
+ modelId: "openai/text-embedding-3-large",
+ openRouterOptions: {
+ apiKey: "test-api-key",
+ openRouterBaseUrl: testBaseUrl,
+ },
+ }),
+ }
+
+ const { CodeIndexServiceFactory } = await import("../service-factory")
+ const factory = new CodeIndexServiceFactory(configManager, workspacePath, cacheManager)
+ const embedder = factory.createEmbedder()
+ const result = await embedder.validateConfiguration()
+ expect(validateConfiguration).toHaveBeenCalled()
+ expect(result).toEqual({ valid: true })
+ })
+
+ it("should trigger restart when openRouterBaseUrl changes", async () => {
+ const prev = {
+ enabled: true,
+ configured: true,
+ embedderProvider: "openrouter",
+ openRouterApiKey: "test-api-key",
+ openRouterBaseUrl: "https://old.openrouter.ai/api/v1",
+ }
+ const configManagerModule = await import("../config-manager")
+ const mgr = new configManagerModule.CodeIndexConfigManager({
+ getGlobalState: vi.fn().mockReturnValue({
+ codebaseIndexEnabled: true,
+ codebaseIndexEmbedderProvider: "openrouter",
+ codebaseIndexOpenRouterEmbedderBaseUrl: "https://new.openrouter.ai/api/v1",
+ }),
+ getSecret: vi.fn().mockReturnValue("test-api-key"),
+ refreshSecrets: vi.fn(),
+ })
+ mgr._loadAndSetConfiguration()
+ const requiresRestart = mgr.doesConfigChangeRequireRestart(prev)
+ expect(requiresRestart).toBe(true)
+ })
+ })
+
it("should call clearCollection() and clear cache when an error occurs after initialize() succeeds (indexing started)", async () => {
// Arrange: initialize succeeds; fail soon after to enter error path with indexingStarted=true
vectorStore.initialize.mockResolvedValue(false) // existing collection
diff --git a/src/services/code-index/embedders/__tests__/openrouter.spec.ts b/src/services/code-index/embedders/__tests__/openrouter.spec.ts
index 250cc4bf01..beff9a513b 100644
--- a/src/services/code-index/embedders/__tests__/openrouter.spec.ts
+++ b/src/services/code-index/embedders/__tests__/openrouter.spec.ts
@@ -374,6 +374,58 @@ describe("OpenRouterEmbedder", () => {
})
})
+ // Tests for codebaseIndexOpenRouterEmbedderBaseUrl validation
+
+ describe("OpenRouterEmbedder custom base URL validation", () => {
+ it("should validate configuration with custom base URL", async () => {
+ const customBaseUrl = "https://custom.openrouter.example/api/v1"
+ const testEmbedding = new Float32Array([0.1, 0.2])
+ const base64String = Buffer.from(testEmbedding.buffer).toString("base64")
+ const mockResponse = {
+ data: [{ embedding: base64String }],
+ usage: { prompt_tokens: 1, total_tokens: 1 },
+ }
+ mockEmbeddingsCreate.mockResolvedValue(mockResponse)
+ const embedderWithCustomBase = new OpenRouterEmbedder(
+ mockApiKey,
+ undefined,
+ undefined,
+ undefined,
+ customBaseUrl,
+ )
+ const result = await embedderWithCustomBase.validateConfiguration()
+ expect(result.valid).toBe(true)
+ expect(result.error).toBeUndefined()
+ expect(MockedOpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: customBaseUrl }))
+ })
+
+ it("should handle error for invalid custom base URL", async () => {
+ const invalidBaseUrl = "not-a-valid-url"
+ ;(MockedOpenAI as any).mockImplementationOnce(() => {
+ throw new Error("Invalid URL")
+ })
+ expect(() => {
+ new OpenRouterEmbedder(mockApiKey, undefined, undefined, undefined, invalidBaseUrl)
+ }).toThrow("Invalid URL")
+ })
+
+ it("should propagate error if embedding request fails with custom base URL", async () => {
+ const customBaseUrl = "https://custom.openrouter.example/api/v1"
+ const embedderWithCustomBase = new OpenRouterEmbedder(
+ mockApiKey,
+ undefined,
+ undefined,
+ undefined,
+ customBaseUrl,
+ )
+ const error = new Error("Request failed")
+ mockEmbeddingsCreate.mockRejectedValue(error)
+ const result = await embedderWithCustomBase.validateConfiguration()
+ expect(result.valid).toBe(false)
+ expect(result.error).toBe("Request failed")
+ })
+ })
+
describe("integration with shared models", () => {
it("should work with defined OpenRouter models", () => {
const openRouterModels = [
diff --git a/webview-ui/src/components/chat/__tests__/CodeIndexPopover.auto-populate.spec.tsx b/webview-ui/src/components/chat/__tests__/CodeIndexPopover.auto-populate.spec.tsx
index 818a771281..658b0ab529 100644
--- a/webview-ui/src/components/chat/__tests__/CodeIndexPopover.auto-populate.spec.tsx
+++ b/webview-ui/src/components/chat/__tests__/CodeIndexPopover.auto-populate.spec.tsx
@@ -309,6 +309,59 @@ describe("CodeIndexPopover - Auto-population Feature Logic", () => {
awsProfile: "default",
}
+ /**
+ * UI/component tests for codebaseIndexOpenRouterEmbedderBaseUrl field
+ */
+ describe("CodeIndexPopover - OpenRouter Embedder Base URL Field", () => {
+ /**
+ * Test 1: Renders VSCodeTextField for codebaseIndexOpenRouterEmbedderBaseUrl
+ */
+ test("renders VSCodeTextField for codebaseIndexOpenRouterEmbedderBaseUrl", () => {
+ const settings = {
+ codebaseIndexEmbedderProvider: "openrouter",
+ codebaseIndexOpenRouterEmbedderBaseUrl: "https://openrouter.ai/api/v1",
+ }
+ const rendered = ``
+ expect(rendered).toContain('data-testid="openrouter-base-url"')
+ expect(rendered).toContain(settings.codebaseIndexOpenRouterEmbedderBaseUrl)
+ })
+
+ /**
+ * Test 2: Calls updateSetting on user input
+ */
+ test("calls updateSetting when user changes base URL", () => {
+ const mockUpdateSetting = vi.fn()
+ const event = { target: { value: "https://custom.openrouter.ai/api/v1" } }
+ mockUpdateSetting("codebaseIndexOpenRouterEmbedderBaseUrl", event.target.value)
+ expect(mockUpdateSetting).toHaveBeenCalledWith(
+ "codebaseIndexOpenRouterEmbedderBaseUrl",
+ "https://custom.openrouter.ai/api/v1",
+ )
+ })
+
+ /**
+ * Test 3: Validation error shown for invalid URL
+ */
+ test("shows validation error for invalid OpenRouter base URL", () => {
+ const invalidUrl = "not-a-url"
+ const formErrors = { codebaseIndexOpenRouterEmbedderBaseUrl: "Invalid URL" }
+ const rendered = ``
+ expect(rendered).toContain("border-red-500")
+ expect(formErrors.codebaseIndexOpenRouterEmbedderBaseUrl).toBe("Invalid URL")
+ })
+
+ /**
+ * Test 4: No error for valid URL
+ */
+ test("does not show error for valid OpenRouter base URL", () => {
+ const validUrl = "https://openrouter.ai/api/v1"
+ const formErrors: Record = {}
+ const rendered = ``
+ expect(rendered).not.toContain("border-red-500")
+ expect(formErrors.codebaseIndexOpenRouterEmbedderBaseUrl).toBeUndefined()
+ })
+ })
+
// Simulate the onValueChange logic
const value = "bedrock"