diff --git a/packages/types/src/codebase-index.ts b/packages/types/src/codebase-index.ts index 61009ba3011..d9170115bba 100644 --- a/packages/types/src/codebase-index.ts +++ b/packages/types/src/codebase-index.ts @@ -49,6 +49,7 @@ export const codebaseIndexConfigSchema = z.object({ codebaseIndexBedrockRegion: z.string().optional(), codebaseIndexBedrockProfile: z.string().optional(), // OpenRouter specific fields + codebaseIndexOpenRouterEmbedderBaseUrl: z.string().optional(), codebaseIndexOpenRouterSpecificProvider: z.string().optional(), }) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index d19a6b6ea6e..69cc824ef6a 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2046,6 +2046,7 @@ export class ClineProvider codebaseIndexBedrockRegion: codebaseIndexConfig?.codebaseIndexBedrockRegion, codebaseIndexBedrockProfile: codebaseIndexConfig?.codebaseIndexBedrockProfile, codebaseIndexOpenRouterSpecificProvider: codebaseIndexConfig?.codebaseIndexOpenRouterSpecificProvider, + codebaseIndexOpenRouterEmbedderBaseUrl: codebaseIndexConfig?.codebaseIndexOpenRouterEmbedderBaseUrl, }, // Only set mdmCompliant if there's an actual MDM policy // undefined means no MDM policy, true means compliant, false means non-compliant @@ -2292,6 +2293,8 @@ export class ClineProvider codebaseIndexSearchMinScore: stateValues.codebaseIndexConfig?.codebaseIndexSearchMinScore, codebaseIndexBedrockRegion: stateValues.codebaseIndexConfig?.codebaseIndexBedrockRegion, codebaseIndexBedrockProfile: stateValues.codebaseIndexConfig?.codebaseIndexBedrockProfile, + codebaseIndexOpenRouterEmbedderBaseUrl: + stateValues.codebaseIndexConfig?.codebaseIndexOpenRouterEmbedderBaseUrl, codebaseIndexOpenRouterSpecificProvider: stateValues.codebaseIndexConfig?.codebaseIndexOpenRouterSpecificProvider, }, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 52ec8146552..6d89cd21ad4 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2451,6 +2451,7 @@ export const webviewMessageHandler = async ( codebaseIndexSearchMaxResults: settings.codebaseIndexSearchMaxResults, codebaseIndexSearchMinScore: settings.codebaseIndexSearchMinScore, codebaseIndexOpenRouterSpecificProvider: settings.codebaseIndexOpenRouterSpecificProvider, + codebaseIndexOpenRouterEmbedderBaseUrl: settings.codebaseIndexOpenRouterEmbedderBaseUrl, } // Save global state first diff --git a/src/services/code-index/__tests__/config-manager.spec.ts b/src/services/code-index/__tests__/config-manager.spec.ts index 27815c0bef0..c0bd7ec1ddf 100644 --- a/src/services/code-index/__tests__/config-manager.spec.ts +++ b/src/services/code-index/__tests__/config-manager.spec.ts @@ -1550,6 +1550,118 @@ describe("CodeIndexConfigManager", () => { mockedGetModelScoreThreshold.mockReturnValue(undefined) }) + 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) + }) + it("should load configuration and return proper structure", async () => { const mockConfigValues = { codebaseIndexEnabled: true, diff --git a/src/services/code-index/__tests__/orchestrator.spec.ts b/src/services/code-index/__tests__/orchestrator.spec.ts index aab1ef888d3..33418c5d954 100644 --- a/src/services/code-index/__tests__/orchestrator.spec.ts +++ b/src/services/code-index/__tests__/orchestrator.spec.ts @@ -129,6 +129,112 @@ 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, + }, + }), + // Use a valid EmbedderProvider value + } + + 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(testBaseUrl) + }) + + 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" as import("../interfaces/manager").EmbedderProvider, + openRouterApiKey: "test-api-key", + openRouterBaseUrl: "https://old.openrouter.ai/api/v1", + } + const configManagerModule = await import("../config-manager") + // Provide a full ContextProxy mock with required properties/methods + const mockContextProxy = { + 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(), + setValue: vi.fn(), + setValues: vi.fn(), + getValue: vi.fn(), + getProviderSettings: vi.fn().mockReturnValue({}), + setProviderSettings: vi.fn(), + // Additional required properties/methods for ContextProxy + originalContext: {}, + stateCache: {}, + secretCache: {}, + _isInitialized: true, + isInitialized: true, + extensionUri: {}, + extensionPath: "", + globalStorageUri: {}, + logUri: {}, + extension: {}, + extensionMode: 1, + } + const mgr = new configManagerModule.CodeIndexConfigManager(mockContextProxy as any) + await mgr.loadConfiguration() + 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/config-manager.ts b/src/services/code-index/config-manager.ts index e7f239e621f..1eab0a310dd 100644 --- a/src/services/code-index/config-manager.ts +++ b/src/services/code-index/config-manager.ts @@ -21,7 +21,7 @@ export class CodeIndexConfigManager { private mistralOptions?: { apiKey: string } private vercelAiGatewayOptions?: { apiKey: string } private bedrockOptions?: { region: string; profile?: string } - private openRouterOptions?: { apiKey: string; specificProvider?: string } + private openRouterOptions?: { apiKey: string; specificProvider?: string; openRouterBaseUrl?: string } private qdrantUrl?: string = "http://localhost:6333" private qdrantApiKey?: string private searchMinScore?: number @@ -78,6 +78,7 @@ export class CodeIndexConfigManager { const bedrockRegion = codebaseIndexConfig.codebaseIndexBedrockRegion ?? "us-east-1" const bedrockProfile = codebaseIndexConfig.codebaseIndexBedrockProfile ?? "" const openRouterApiKey = this.contextProxy?.getSecret("codebaseIndexOpenRouterApiKey") ?? "" + const openRouterEmbedderBaseUrl = codebaseIndexConfig.codebaseIndexOpenRouterEmbedderBaseUrl ?? "" const openRouterSpecificProvider = codebaseIndexConfig.codebaseIndexOpenRouterSpecificProvider ?? "" // Update instance variables with configuration @@ -142,7 +143,11 @@ export class CodeIndexConfigManager { this.mistralOptions = mistralApiKey ? { apiKey: mistralApiKey } : undefined this.vercelAiGatewayOptions = vercelAiGatewayApiKey ? { apiKey: vercelAiGatewayApiKey } : undefined this.openRouterOptions = openRouterApiKey - ? { apiKey: openRouterApiKey, specificProvider: openRouterSpecificProvider || undefined } + ? { + apiKey: openRouterApiKey, + openRouterBaseUrl: openRouterEmbedderBaseUrl, + specificProvider: openRouterSpecificProvider || undefined, + } : undefined // Set bedrockOptions if region is provided (profile is optional) this.bedrockOptions = bedrockRegion @@ -167,7 +172,7 @@ export class CodeIndexConfigManager { mistralOptions?: { apiKey: string } vercelAiGatewayOptions?: { apiKey: string } bedrockOptions?: { region: string; profile?: string } - openRouterOptions?: { apiKey: string } + openRouterOptions?: { apiKey: string; specificProvider?: string; openRouterBaseUrl?: string } qdrantUrl?: string qdrantApiKey?: string searchMinScore?: number @@ -191,6 +196,7 @@ export class CodeIndexConfigManager { bedrockRegion: this.bedrockOptions?.region ?? "", bedrockProfile: this.bedrockOptions?.profile ?? "", openRouterApiKey: this.openRouterOptions?.apiKey ?? "", + openRouterBaseUrl: this.openRouterOptions?.openRouterBaseUrl ?? "", openRouterSpecificProvider: this.openRouterOptions?.specificProvider ?? "", qdrantUrl: this.qdrantUrl ?? "", qdrantApiKey: this.qdrantApiKey ?? "", @@ -310,6 +316,7 @@ export class CodeIndexConfigManager { const prevBedrockRegion = prev?.bedrockRegion ?? "" const prevBedrockProfile = prev?.bedrockProfile ?? "" const prevOpenRouterApiKey = prev?.openRouterApiKey ?? "" + const prevOpenRouterBaseUrl = prev?.openRouterBaseUrl ?? "" const prevOpenRouterSpecificProvider = prev?.openRouterSpecificProvider ?? "" const prevQdrantUrl = prev?.qdrantUrl ?? "" const prevQdrantApiKey = prev?.qdrantApiKey ?? "" @@ -352,6 +359,7 @@ export class CodeIndexConfigManager { const currentBedrockRegion = this.bedrockOptions?.region ?? "" const currentBedrockProfile = this.bedrockOptions?.profile ?? "" const currentOpenRouterApiKey = this.openRouterOptions?.apiKey ?? "" + const currentOpenRouterBaseUrl = this.openRouterOptions?.openRouterBaseUrl ?? "" const currentOpenRouterSpecificProvider = this.openRouterOptions?.specificProvider ?? "" const currentQdrantUrl = this.qdrantUrl ?? "" const currentQdrantApiKey = this.qdrantApiKey ?? "" @@ -396,6 +404,10 @@ export class CodeIndexConfigManager { return true } + if (prevOpenRouterBaseUrl !== currentOpenRouterBaseUrl) { + return true + } + // Check for model dimension changes (generic for all providers) if (prevModelDimension !== currentModelDimension) { return true diff --git a/src/services/code-index/embedders/__tests__/openrouter.spec.ts b/src/services/code-index/embedders/__tests__/openrouter.spec.ts index 250cc4bf01e..beff9a513b7 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/src/services/code-index/embedders/openrouter.ts b/src/services/code-index/embedders/openrouter.ts index 2ffdd7afb64..500c9557ad8 100644 --- a/src/services/code-index/embedders/openrouter.ts +++ b/src/services/code-index/embedders/openrouter.ts @@ -40,7 +40,6 @@ export class OpenRouterEmbedder implements IEmbedder { private readonly defaultModelId: string private readonly apiKey: string private readonly maxItemTokens: number - private readonly baseUrl: string = "https://openrouter.ai/api/v1" private readonly specificProvider?: string // Global rate limiting state shared across all instances @@ -58,13 +57,22 @@ export class OpenRouterEmbedder implements IEmbedder { * @param apiKey The API key for authentication * @param modelId Optional model identifier (defaults to "openai/text-embedding-3-large") * @param maxItemTokens Optional maximum tokens per item (defaults to MAX_ITEM_TOKENS) + * @param openRouterBaseUrl Optional custom OpenRouter base URL * @param specificProvider Optional specific provider to route requests to */ - constructor(apiKey: string, modelId?: string, maxItemTokens?: number, specificProvider?: string) { + constructor( + apiKey: string, + modelId?: string, + maxItemTokens?: number, + specificProvider?: string, + openRouterBaseUrl?: string, + ) { if (!apiKey) { throw new Error(t("embeddings:validation.apiKeyRequired")) } + const baseUrl = openRouterBaseUrl || "https://openrouter.ai/api/v1" + this.apiKey = apiKey // Only set specificProvider if it's not the default value this.specificProvider = @@ -73,7 +81,7 @@ export class OpenRouterEmbedder implements IEmbedder { // Wrap OpenAI client creation to handle invalid API key characters try { this.embeddingsClient = new OpenAI({ - baseURL: this.baseUrl, + baseURL: baseUrl, apiKey: apiKey, defaultHeaders: { "HTTP-Referer": "https://github.com/RooCodeInc/Roo-Code", diff --git a/src/services/code-index/interfaces/config.ts b/src/services/code-index/interfaces/config.ts index f52f98aaa0d..19bf2e42524 100644 --- a/src/services/code-index/interfaces/config.ts +++ b/src/services/code-index/interfaces/config.ts @@ -16,7 +16,7 @@ export interface CodeIndexConfig { mistralOptions?: { apiKey: string } vercelAiGatewayOptions?: { apiKey: string } bedrockOptions?: { region: string; profile?: string } - openRouterOptions?: { apiKey: string; specificProvider?: string } + openRouterOptions?: { apiKey: string; specificProvider?: string; openRouterBaseUrl?: string } qdrantUrl?: string qdrantApiKey?: string searchMinScore?: number @@ -42,6 +42,7 @@ export type PreviousConfigSnapshot = { bedrockRegion?: string bedrockProfile?: string openRouterApiKey?: string + openRouterBaseUrl?: string openRouterSpecificProvider?: string qdrantUrl?: string qdrantApiKey?: string diff --git a/src/services/code-index/service-factory.ts b/src/services/code-index/service-factory.ts index c98c65d4c19..09181975e8a 100644 --- a/src/services/code-index/service-factory.ts +++ b/src/services/code-index/service-factory.ts @@ -96,6 +96,7 @@ export class CodeIndexServiceFactory { config.modelId, undefined, // maxItemTokens config.openRouterOptions.specificProvider, + config.openRouterOptions.openRouterBaseUrl, ) } diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index d96fbd871ce..e95980d97c1 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -255,6 +255,7 @@ export interface WebviewMessage { codebaseIndexBedrockProfile?: string codebaseIndexSearchMaxResults?: number codebaseIndexSearchMinScore?: number + codebaseIndexOpenRouterEmbedderBaseUrl?: string codebaseIndexOpenRouterSpecificProvider?: string // OpenRouter provider routing // Secret settings diff --git a/webview-ui/src/components/chat/CodeIndexPopover.tsx b/webview-ui/src/components/chat/CodeIndexPopover.tsx index 368f0395eaf..678d413814c 100644 --- a/webview-ui/src/components/chat/CodeIndexPopover.tsx +++ b/webview-ui/src/components/chat/CodeIndexPopover.tsx @@ -53,6 +53,7 @@ import { // Default URLs for providers const DEFAULT_QDRANT_URL = "http://localhost:6333" const DEFAULT_OLLAMA_URL = "http://localhost:11434" +const DEFAULT_OPENROUTER_URL = "https://openrouter.ai/api/v1" interface CodeIndexPopoverProps { children: React.ReactNode @@ -83,6 +84,7 @@ interface LocalCodeIndexSettings { codebaseIndexMistralApiKey?: string codebaseIndexVercelAiGatewayApiKey?: string codebaseIndexOpenRouterApiKey?: string + codebaseIndexOpenRouterEmbedderBaseUrl?: string codebaseIndexOpenRouterSpecificProvider?: string } @@ -227,6 +229,7 @@ export const CodeIndexPopover: React.FC = ({ codebaseIndexMistralApiKey: "", codebaseIndexVercelAiGatewayApiKey: "", codebaseIndexOpenRouterApiKey: "", + codebaseIndexOpenRouterEmbedderBaseUrl: "", codebaseIndexOpenRouterSpecificProvider: "", }) @@ -266,6 +269,8 @@ export const CodeIndexPopover: React.FC = ({ codebaseIndexMistralApiKey: "", codebaseIndexVercelAiGatewayApiKey: "", codebaseIndexOpenRouterApiKey: "", + codebaseIndexOpenRouterEmbedderBaseUrl: + codebaseIndexConfig.codebaseIndexOpenRouterEmbedderBaseUrl || "", codebaseIndexOpenRouterSpecificProvider: codebaseIndexConfig.codebaseIndexOpenRouterSpecificProvider || "", } @@ -1321,6 +1326,41 @@ export const CodeIndexPopover: React.FC = ({ {currentSettings.codebaseIndexEmbedderProvider === "openrouter" && ( <> +
+ + + updateSetting( + "codebaseIndexOpenRouterEmbedderBaseUrl", + e.target.value, + ) + } + onBlur={(e: any) => { + // Set default OpenRouter URL if field is empty + if (!e.target.value.trim()) { + e.target.value = DEFAULT_OPENROUTER_URL + updateSetting( + "codebaseIndexOpenRouterEmbedderBaseUrl", + DEFAULT_OPENROUTER_URL, + ) + } + }} + placeholder={t("settings:codeIndex.openRouterBaseUrlPlaceholder")} + className={cn("w-full", { + "border-red-500": + formErrors.codebaseIndexOpenRouterEmbedderBaseUrl, + })} + /> + {formErrors.codebaseIndexOpenRouterEmbedderBaseUrl && ( +

+ {formErrors.codebaseIndexOpenRouterEmbedderBaseUrl} +

+ )} +
+