Просмотр исходного кода

feat: Add provider routing selection for OpenRouter embeddings (#9144) (#9693)

Co-authored-by: Sannidhya <[email protected]>
SannidhyaSah 1 месяц назад
Родитель
Сommit
873a763ea7
29 измененных файлов с 309 добавлено и 11 удалено
  1. 2 0
      packages/types/src/codebase-index.ts
  2. 3 0
      src/core/webview/ClineProvider.ts
  3. 1 0
      src/core/webview/webviewMessageHandler.ts
  4. 13 2
      src/services/code-index/config-manager.ts
  5. 119 1
      src/services/code-index/embedders/__tests__/openrouter.spec.ts
  6. 41 5
      src/services/code-index/embedders/openrouter.ts
  7. 2 1
      src/services/code-index/interfaces/config.ts
  8. 6 1
      src/services/code-index/service-factory.ts
  9. 1 0
      src/shared/WebviewMessage.ts
  10. 70 0
      webview-ui/src/components/chat/CodeIndexPopover.tsx
  11. 2 0
      webview-ui/src/i18n/locales/ca/settings.json
  12. 2 0
      webview-ui/src/i18n/locales/de/settings.json
  13. 2 0
      webview-ui/src/i18n/locales/en/settings.json
  14. 2 0
      webview-ui/src/i18n/locales/es/settings.json
  15. 2 0
      webview-ui/src/i18n/locales/fr/settings.json
  16. 2 0
      webview-ui/src/i18n/locales/hi/settings.json
  17. 2 0
      webview-ui/src/i18n/locales/id/settings.json
  18. 2 0
      webview-ui/src/i18n/locales/it/settings.json
  19. 2 0
      webview-ui/src/i18n/locales/ja/settings.json
  20. 2 0
      webview-ui/src/i18n/locales/ko/settings.json
  21. 2 0
      webview-ui/src/i18n/locales/nl/settings.json
  22. 2 0
      webview-ui/src/i18n/locales/pl/settings.json
  23. 2 0
      webview-ui/src/i18n/locales/pt-BR/settings.json
  24. 2 0
      webview-ui/src/i18n/locales/ru/settings.json
  25. 2 0
      webview-ui/src/i18n/locales/tr/settings.json
  26. 2 0
      webview-ui/src/i18n/locales/vi/settings.json
  27. 2 0
      webview-ui/src/i18n/locales/zh-CN/settings.json
  28. 2 0
      webview-ui/src/i18n/locales/zh-TW/settings.json
  29. 15 1
      webview-ui/src/utils/test-utils.tsx

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

@@ -48,6 +48,8 @@ export const codebaseIndexConfigSchema = z.object({
 	// Bedrock specific fields
 	codebaseIndexBedrockRegion: z.string().optional(),
 	codebaseIndexBedrockProfile: z.string().optional(),
+	// OpenRouter specific fields
+	codebaseIndexOpenRouterSpecificProvider: z.string().optional(),
 })
 
 export type CodebaseIndexConfig = z.infer<typeof codebaseIndexConfigSchema>

+ 3 - 0
src/core/webview/ClineProvider.ts

@@ -2079,6 +2079,7 @@ export class ClineProvider
 				codebaseIndexSearchMinScore: codebaseIndexConfig?.codebaseIndexSearchMinScore,
 				codebaseIndexBedrockRegion: codebaseIndexConfig?.codebaseIndexBedrockRegion,
 				codebaseIndexBedrockProfile: codebaseIndexConfig?.codebaseIndexBedrockProfile,
+				codebaseIndexOpenRouterSpecificProvider: codebaseIndexConfig?.codebaseIndexOpenRouterSpecificProvider,
 			},
 			// Only set mdmCompliant if there's an actual MDM policy
 			// undefined means no MDM policy, true means compliant, false means non-compliant
@@ -2310,6 +2311,8 @@ export class ClineProvider
 				codebaseIndexSearchMinScore: stateValues.codebaseIndexConfig?.codebaseIndexSearchMinScore,
 				codebaseIndexBedrockRegion: stateValues.codebaseIndexConfig?.codebaseIndexBedrockRegion,
 				codebaseIndexBedrockProfile: stateValues.codebaseIndexConfig?.codebaseIndexBedrockProfile,
+				codebaseIndexOpenRouterSpecificProvider:
+					stateValues.codebaseIndexConfig?.codebaseIndexOpenRouterSpecificProvider,
 			},
 			profileThresholds: stateValues.profileThresholds ?? {},
 			includeDiagnosticMessages: stateValues.includeDiagnosticMessages ?? true,

+ 1 - 0
src/core/webview/webviewMessageHandler.ts

@@ -2373,6 +2373,7 @@ export const webviewMessageHandler = async (
 					codebaseIndexBedrockProfile: settings.codebaseIndexBedrockProfile,
 					codebaseIndexSearchMaxResults: settings.codebaseIndexSearchMaxResults,
 					codebaseIndexSearchMinScore: settings.codebaseIndexSearchMinScore,
+					codebaseIndexOpenRouterSpecificProvider: settings.codebaseIndexOpenRouterSpecificProvider,
 				}
 
 				// Save global state first

+ 13 - 2
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 }
+	private openRouterOptions?: { apiKey: string; specificProvider?: 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 openRouterSpecificProvider = codebaseIndexConfig.codebaseIndexOpenRouterSpecificProvider ?? ""
 
 		// Update instance variables with configuration
 		this.codebaseIndexEnabled = codebaseIndexEnabled ?? false
@@ -140,7 +141,9 @@ export class CodeIndexConfigManager {
 		this.geminiOptions = geminiApiKey ? { apiKey: geminiApiKey } : undefined
 		this.mistralOptions = mistralApiKey ? { apiKey: mistralApiKey } : undefined
 		this.vercelAiGatewayOptions = vercelAiGatewayApiKey ? { apiKey: vercelAiGatewayApiKey } : undefined
-		this.openRouterOptions = openRouterApiKey ? { apiKey: openRouterApiKey } : undefined
+		this.openRouterOptions = openRouterApiKey
+			? { apiKey: openRouterApiKey, specificProvider: openRouterSpecificProvider || undefined }
+			: undefined
 		// Set bedrockOptions if region is provided (profile is optional)
 		this.bedrockOptions = bedrockRegion
 			? { region: bedrockRegion, profile: bedrockProfile || undefined }
@@ -188,6 +191,7 @@ export class CodeIndexConfigManager {
 			bedrockRegion: this.bedrockOptions?.region ?? "",
 			bedrockProfile: this.bedrockOptions?.profile ?? "",
 			openRouterApiKey: this.openRouterOptions?.apiKey ?? "",
+			openRouterSpecificProvider: this.openRouterOptions?.specificProvider ?? "",
 			qdrantUrl: this.qdrantUrl ?? "",
 			qdrantApiKey: this.qdrantApiKey ?? "",
 		}
@@ -306,6 +310,7 @@ export class CodeIndexConfigManager {
 		const prevBedrockRegion = prev?.bedrockRegion ?? ""
 		const prevBedrockProfile = prev?.bedrockProfile ?? ""
 		const prevOpenRouterApiKey = prev?.openRouterApiKey ?? ""
+		const prevOpenRouterSpecificProvider = prev?.openRouterSpecificProvider ?? ""
 		const prevQdrantUrl = prev?.qdrantUrl ?? ""
 		const prevQdrantApiKey = prev?.qdrantApiKey ?? ""
 
@@ -347,6 +352,7 @@ export class CodeIndexConfigManager {
 		const currentBedrockRegion = this.bedrockOptions?.region ?? ""
 		const currentBedrockProfile = this.bedrockOptions?.profile ?? ""
 		const currentOpenRouterApiKey = this.openRouterOptions?.apiKey ?? ""
+		const currentOpenRouterSpecificProvider = this.openRouterOptions?.specificProvider ?? ""
 		const currentQdrantUrl = this.qdrantUrl ?? ""
 		const currentQdrantApiKey = this.qdrantApiKey ?? ""
 
@@ -385,6 +391,11 @@ export class CodeIndexConfigManager {
 			return true
 		}
 
+		// OpenRouter specific provider change
+		if (prevOpenRouterSpecificProvider !== currentOpenRouterSpecificProvider) {
+			return true
+		}
+
 		// Check for model dimension changes (generic for all providers)
 		if (prevModelDimension !== currentModelDimension) {
 			return true

+ 119 - 1
src/services/code-index/embedders/__tests__/openrouter.spec.ts

@@ -1,7 +1,7 @@
 import type { MockedClass, MockedFunction } from "vitest"
 import { describe, it, expect, beforeEach, vi } from "vitest"
 import { OpenAI } from "openai"
-import { OpenRouterEmbedder } from "../openrouter"
+import { OpenRouterEmbedder, OPENROUTER_DEFAULT_PROVIDER_NAME } from "../openrouter"
 import { getModelDimension, getDefaultModelId } from "../../../../shared/embeddingModels"
 
 // Mock the OpenAI SDK
@@ -95,6 +95,16 @@ describe("OpenRouterEmbedder", () => {
 				},
 			})
 		})
+
+		it("should accept specificProvider parameter", () => {
+			const embedder = new OpenRouterEmbedder(mockApiKey, undefined, undefined, "together")
+			expect(embedder).toBeInstanceOf(OpenRouterEmbedder)
+		})
+
+		it("should ignore default provider name as specificProvider", () => {
+			const embedder = new OpenRouterEmbedder(mockApiKey, undefined, undefined, OPENROUTER_DEFAULT_PROVIDER_NAME)
+			expect(embedder).toBeInstanceOf(OpenRouterEmbedder)
+		})
 	})
 
 	describe("embedderInfo", () => {
@@ -205,6 +215,77 @@ describe("OpenRouterEmbedder", () => {
 				encoding_format: "base64",
 			})
 		})
+
+		it("should include provider routing when specificProvider is set", async () => {
+			const specificProvider = "together"
+			const embedderWithProvider = new OpenRouterEmbedder(mockApiKey, undefined, undefined, specificProvider)
+
+			const testEmbedding = new Float32Array([0.25, 0.5])
+			const base64String = Buffer.from(testEmbedding.buffer).toString("base64")
+
+			const mockResponse = {
+				data: [
+					{
+						embedding: base64String,
+					},
+				],
+				usage: {
+					prompt_tokens: 5,
+					total_tokens: 5,
+				},
+			}
+
+			mockEmbeddingsCreate.mockResolvedValue(mockResponse)
+
+			await embedderWithProvider.createEmbeddings(["test"])
+
+			// Verify the embeddings.create was called with provider routing
+			expect(mockEmbeddingsCreate).toHaveBeenCalledWith({
+				input: ["test"],
+				model: "openai/text-embedding-3-large",
+				encoding_format: "base64",
+				provider: {
+					order: [specificProvider],
+					only: [specificProvider],
+					allow_fallbacks: false,
+				},
+			})
+		})
+
+		it("should not include provider routing when specificProvider is default", async () => {
+			const embedderWithDefaultProvider = new OpenRouterEmbedder(
+				mockApiKey,
+				undefined,
+				undefined,
+				OPENROUTER_DEFAULT_PROVIDER_NAME,
+			)
+
+			const testEmbedding = new Float32Array([0.25, 0.5])
+			const base64String = Buffer.from(testEmbedding.buffer).toString("base64")
+
+			const mockResponse = {
+				data: [
+					{
+						embedding: base64String,
+					},
+				],
+				usage: {
+					prompt_tokens: 5,
+					total_tokens: 5,
+				},
+			}
+
+			mockEmbeddingsCreate.mockResolvedValue(mockResponse)
+
+			await embedderWithDefaultProvider.createEmbeddings(["test"])
+
+			// Verify the embeddings.create was called without provider routing
+			expect(mockEmbeddingsCreate).toHaveBeenCalledWith({
+				input: ["test"],
+				model: "openai/text-embedding-3-large",
+				encoding_format: "base64",
+			})
+		})
 	})
 
 	describe("validateConfiguration", () => {
@@ -254,6 +335,43 @@ describe("OpenRouterEmbedder", () => {
 			expect(result.valid).toBe(false)
 			expect(result.error).toBe("embeddings:validation.authenticationFailed")
 		})
+
+		it("should validate configuration with specificProvider", async () => {
+			const specificProvider = "openai"
+			const embedderWithProvider = new OpenRouterEmbedder(mockApiKey, undefined, undefined, specificProvider)
+
+			const testEmbedding = new Float32Array([0.25, 0.5])
+			const base64String = Buffer.from(testEmbedding.buffer).toString("base64")
+
+			const mockResponse = {
+				data: [
+					{
+						embedding: base64String,
+					},
+				],
+				usage: {
+					prompt_tokens: 1,
+					total_tokens: 1,
+				},
+			}
+
+			mockEmbeddingsCreate.mockResolvedValue(mockResponse)
+
+			const result = await embedderWithProvider.validateConfiguration()
+
+			expect(result.valid).toBe(true)
+			expect(result.error).toBeUndefined()
+			expect(mockEmbeddingsCreate).toHaveBeenCalledWith({
+				input: ["test"],
+				model: "openai/text-embedding-3-large",
+				encoding_format: "base64",
+				provider: {
+					order: [specificProvider],
+					only: [specificProvider],
+					allow_fallbacks: false,
+				},
+			})
+		})
 	})
 
 	describe("integration with shared models", () => {

+ 41 - 5
src/services/code-index/embedders/openrouter.ts

@@ -14,6 +14,9 @@ import { TelemetryService } from "@roo-code/telemetry"
 import { Mutex } from "async-mutex"
 import { handleOpenAIError } from "../../../api/providers/utils/openai-error-handler"
 
+// Default provider name when no specific provider is selected
+export const OPENROUTER_DEFAULT_PROVIDER_NAME = "[default]"
+
 interface EmbeddingItem {
 	embedding: string | number[]
 	[key: string]: any
@@ -38,6 +41,7 @@ export class OpenRouterEmbedder implements IEmbedder {
 	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
 	private static globalRateLimitState = {
@@ -54,13 +58,17 @@ 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 specificProvider Optional specific provider to route requests to
 	 */
-	constructor(apiKey: string, modelId?: string, maxItemTokens?: number) {
+	constructor(apiKey: string, modelId?: string, maxItemTokens?: number, specificProvider?: string) {
 		if (!apiKey) {
 			throw new Error(t("embeddings:validation.apiKeyRequired"))
 		}
 
 		this.apiKey = apiKey
+		// Only set specificProvider if it's not the default value
+		this.specificProvider =
+			specificProvider && specificProvider !== OPENROUTER_DEFAULT_PROVIDER_NAME ? specificProvider : undefined
 
 		// Wrap OpenAI client creation to handle invalid API key characters
 		try {
@@ -180,14 +188,28 @@ export class OpenRouterEmbedder implements IEmbedder {
 			await this.waitForGlobalRateLimit()
 
 			try {
-				const response = (await this.embeddingsClient.embeddings.create({
+				// Build the request parameters
+				const requestParams: any = {
 					input: batchTexts,
 					model: model,
 					// OpenAI package (as of v4.78.1) has a parsing issue that truncates embedding dimensions to 256
 					// when processing numeric arrays, which breaks compatibility with models using larger dimensions.
 					// By requesting base64 encoding, we bypass the package's parser and handle decoding ourselves.
 					encoding_format: "base64",
-				})) as OpenRouterEmbeddingResponse
+				}
+
+				// Add provider routing if a specific provider is set
+				if (this.specificProvider) {
+					requestParams.provider = {
+						order: [this.specificProvider],
+						only: [this.specificProvider],
+						allow_fallbacks: false,
+					}
+				}
+
+				const response = (await this.embeddingsClient.embeddings.create(
+					requestParams,
+				)) as OpenRouterEmbeddingResponse
 
 				// Convert base64 embeddings to float32 arrays
 				const processedEmbeddings = response.data.map((item: EmbeddingItem) => {
@@ -274,11 +296,25 @@ export class OpenRouterEmbedder implements IEmbedder {
 				const testTexts = ["test"]
 				const modelToUse = this.defaultModelId
 
-				const response = (await this.embeddingsClient.embeddings.create({
+				// Build the request parameters
+				const requestParams: any = {
 					input: testTexts,
 					model: modelToUse,
 					encoding_format: "base64",
-				})) as OpenRouterEmbeddingResponse
+				}
+
+				// Add provider routing if a specific provider is set
+				if (this.specificProvider) {
+					requestParams.provider = {
+						order: [this.specificProvider],
+						only: [this.specificProvider],
+						allow_fallbacks: false,
+					}
+				}
+
+				const response = (await this.embeddingsClient.embeddings.create(
+					requestParams,
+				)) as OpenRouterEmbeddingResponse
 
 				// Check if we got a valid response
 				if (!response?.data || response.data.length === 0) {

+ 2 - 1
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 }
+	openRouterOptions?: { apiKey: string; specificProvider?: string }
 	qdrantUrl?: string
 	qdrantApiKey?: string
 	searchMinScore?: number
@@ -42,6 +42,7 @@ export type PreviousConfigSnapshot = {
 	bedrockRegion?: string
 	bedrockProfile?: string
 	openRouterApiKey?: string
+	openRouterSpecificProvider?: string
 	qdrantUrl?: string
 	qdrantApiKey?: string
 }

+ 6 - 1
src/services/code-index/service-factory.ts

@@ -91,7 +91,12 @@ export class CodeIndexServiceFactory {
 			if (!config.openRouterOptions?.apiKey) {
 				throw new Error(t("embeddings:serviceFactory.openRouterConfigMissing"))
 			}
-			return new OpenRouterEmbedder(config.openRouterOptions.apiKey, config.modelId)
+			return new OpenRouterEmbedder(
+				config.openRouterOptions.apiKey,
+				config.modelId,
+				undefined, // maxItemTokens
+				config.openRouterOptions.specificProvider,
+			)
 		}
 
 		throw new Error(

+ 1 - 0
src/shared/WebviewMessage.ts

@@ -248,6 +248,7 @@ export interface WebviewMessage {
 		codebaseIndexBedrockProfile?: string
 		codebaseIndexSearchMaxResults?: number
 		codebaseIndexSearchMinScore?: number
+		codebaseIndexOpenRouterSpecificProvider?: string // OpenRouter provider routing
 
 		// Secret settings
 		codeIndexOpenAiKey?: string

+ 70 - 0
webview-ui/src/components/chat/CodeIndexPopover.tsx

@@ -45,6 +45,10 @@ import {
 } from "@src/components/ui"
 import { useRooPortal } from "@src/components/ui/hooks/useRooPortal"
 import { useEscapeKey } from "@src/hooks/useEscapeKey"
+import {
+	useOpenRouterModelProviders,
+	OPENROUTER_DEFAULT_PROVIDER_NAME,
+} from "@src/components/ui/hooks/useOpenRouterModelProviders"
 
 // Default URLs for providers
 const DEFAULT_QDRANT_URL = "http://localhost:6333"
@@ -79,6 +83,7 @@ interface LocalCodeIndexSettings {
 	codebaseIndexMistralApiKey?: string
 	codebaseIndexVercelAiGatewayApiKey?: string
 	codebaseIndexOpenRouterApiKey?: string
+	codebaseIndexOpenRouterSpecificProvider?: string
 }
 
 // Validation schema for codebase index settings
@@ -222,6 +227,7 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
 		codebaseIndexMistralApiKey: "",
 		codebaseIndexVercelAiGatewayApiKey: "",
 		codebaseIndexOpenRouterApiKey: "",
+		codebaseIndexOpenRouterSpecificProvider: "",
 	})
 
 	// Initial settings state - stores the settings when popover opens
@@ -260,6 +266,8 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
 				codebaseIndexMistralApiKey: "",
 				codebaseIndexVercelAiGatewayApiKey: "",
 				codebaseIndexOpenRouterApiKey: "",
+				codebaseIndexOpenRouterSpecificProvider:
+					codebaseIndexConfig.codebaseIndexOpenRouterSpecificProvider || "",
 			}
 			setInitialSettings(settings)
 			setCurrentSettings(settings)
@@ -576,6 +584,19 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
 		return models ? Object.keys(models) : []
 	}
 
+	// Fetch OpenRouter model providers for embedding model
+	const { data: openRouterEmbeddingProviders } = useOpenRouterModelProviders(
+		currentSettings.codebaseIndexEmbedderProvider === "openrouter"
+			? currentSettings.codebaseIndexEmbedderModelId
+			: undefined,
+		undefined,
+		{
+			enabled:
+				currentSettings.codebaseIndexEmbedderProvider === "openrouter" &&
+				!!currentSettings.codebaseIndexEmbedderModelId,
+		},
+	)
+
 	const portalContainer = useRooPortal("roo-portal")
 
 	return (
@@ -1360,6 +1381,55 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
 													</p>
 												)}
 											</div>
+
+											{/* Provider Routing for OpenRouter */}
+											{openRouterEmbeddingProviders &&
+												Object.keys(openRouterEmbeddingProviders).length > 0 && (
+													<div className="space-y-2">
+														<label className="text-sm font-medium">
+															<a
+																href="https://openrouter.ai/docs/features/provider-routing"
+																target="_blank"
+																rel="noopener noreferrer"
+																className="flex items-center gap-1 hover:underline">
+																{t("settings:codeIndex.openRouterProviderRoutingLabel")}
+																<span className="codicon codicon-link-external text-xs" />
+															</a>
+														</label>
+														<Select
+															value={
+																currentSettings.codebaseIndexOpenRouterSpecificProvider ||
+																OPENROUTER_DEFAULT_PROVIDER_NAME
+															}
+															onValueChange={(value) =>
+																updateSetting(
+																	"codebaseIndexOpenRouterSpecificProvider",
+																	value,
+																)
+															}>
+															<SelectTrigger className="w-full">
+																<SelectValue />
+															</SelectTrigger>
+															<SelectContent>
+																<SelectItem value={OPENROUTER_DEFAULT_PROVIDER_NAME}>
+																	{OPENROUTER_DEFAULT_PROVIDER_NAME}
+																</SelectItem>
+																{Object.entries(openRouterEmbeddingProviders).map(
+																	([value, { label }]) => (
+																		<SelectItem key={value} value={value}>
+																			{label}
+																		</SelectItem>
+																	),
+																)}
+															</SelectContent>
+														</Select>
+														<p className="text-xs text-vscode-descriptionForeground mt-1 mb-0">
+															{t(
+																"settings:codeIndex.openRouterProviderRoutingDescription",
+															)}
+														</p>
+													</div>
+												)}
 										</>
 									)}
 

+ 2 - 0
webview-ui/src/i18n/locales/ca/settings.json

@@ -89,6 +89,8 @@
 		"openRouterProvider": "OpenRouter",
 		"openRouterApiKeyLabel": "Clau de l'API d'OpenRouter",
 		"openRouterApiKeyPlaceholder": "Introduïu la vostra clau de l'API d'OpenRouter",
+		"openRouterProviderRoutingLabel": "Encaminament de proveïdors d'OpenRouter",
+		"openRouterProviderRoutingDescription": "OpenRouter dirigeix les sol·licituds als millors proveïdors disponibles per al vostre model d'embedding. Per defecte, les sol·licituds s'equilibren entre els principals proveïdors per maximitzar el temps de funcionament. No obstant això, podeu triar un proveïdor específic per utilitzar amb aquest model.",
 		"openaiCompatibleProvider": "Compatible amb OpenAI",
 		"openAiKeyLabel": "Clau API OpenAI",
 		"openAiKeyPlaceholder": "Introduïu la vostra clau API OpenAI",

+ 2 - 0
webview-ui/src/i18n/locales/de/settings.json

@@ -91,6 +91,8 @@
 		"openRouterProvider": "OpenRouter",
 		"openRouterApiKeyLabel": "OpenRouter API-Schlüssel",
 		"openRouterApiKeyPlaceholder": "Gib deinen OpenRouter API-Schlüssel ein",
+		"openRouterProviderRoutingLabel": "OpenRouter Anbieter-Routing",
+		"openRouterProviderRoutingDescription": "OpenRouter leitet Anfragen an die besten verfügbaren Anbieter für dein Embedding-Modell weiter. Standardmäßig werden Anfragen über die Top-Anbieter lastverteilt, um maximale Verfügbarkeit zu gewährleisten. Du kannst jedoch einen bestimmten Anbieter für dieses Modell auswählen.",
 		"mistralProvider": "Mistral",
 		"mistralApiKeyLabel": "API-Schlüssel:",
 		"mistralApiKeyPlaceholder": "Gib deinen Mistral-API-Schlüssel ein",

+ 2 - 0
webview-ui/src/i18n/locales/en/settings.json

@@ -100,6 +100,8 @@
 		"openRouterProvider": "OpenRouter",
 		"openRouterApiKeyLabel": "OpenRouter API Key",
 		"openRouterApiKeyPlaceholder": "Enter your OpenRouter API key",
+		"openRouterProviderRoutingLabel": "OpenRouter Provider Routing",
+		"openRouterProviderRoutingDescription": "OpenRouter routes requests to the best available providers for your embedding model. By default, requests are load balanced across the top providers to maximize uptime. However, you can choose a specific provider to use for this model.",
 		"openaiCompatibleProvider": "OpenAI Compatible",
 		"openAiKeyLabel": "OpenAI API Key",
 		"openAiKeyPlaceholder": "Enter your OpenAI API key",

+ 2 - 0
webview-ui/src/i18n/locales/es/settings.json

@@ -91,6 +91,8 @@
 		"openRouterProvider": "OpenRouter",
 		"openRouterApiKeyLabel": "Clave de API de OpenRouter",
 		"openRouterApiKeyPlaceholder": "Introduce tu clave de API de OpenRouter",
+		"openRouterProviderRoutingLabel": "Enrutamiento de proveedores de OpenRouter",
+		"openRouterProviderRoutingDescription": "OpenRouter dirige las solicitudes a los mejores proveedores disponibles para su modelo de embedding. Por defecto, las solicitudes se equilibran entre los principales proveedores para maximizar el tiempo de actividad. Sin embargo, puede elegir un proveedor específico para este modelo.",
 		"mistralProvider": "Mistral",
 		"mistralApiKeyLabel": "Clave API:",
 		"mistralApiKeyPlaceholder": "Introduce tu clave de API de Mistral",

+ 2 - 0
webview-ui/src/i18n/locales/fr/settings.json

@@ -91,6 +91,8 @@
 		"openRouterProvider": "OpenRouter",
 		"openRouterApiKeyLabel": "Clé d'API OpenRouter",
 		"openRouterApiKeyPlaceholder": "Entrez votre clé d'API OpenRouter",
+		"openRouterProviderRoutingLabel": "Routage des fournisseurs OpenRouter",
+		"openRouterProviderRoutingDescription": "OpenRouter dirige les requêtes vers les meilleurs fournisseurs disponibles pour votre modèle d'embedding. Par défaut, les requêtes sont équilibrées entre les principaux fournisseurs pour maximiser la disponibilité. Cependant, vous pouvez choisir un fournisseur spécifique à utiliser pour ce modèle.",
 		"mistralProvider": "Mistral",
 		"mistralApiKeyLabel": "Clé d'API:",
 		"mistralApiKeyPlaceholder": "Entrez votre clé d'API Mistral",

+ 2 - 0
webview-ui/src/i18n/locales/hi/settings.json

@@ -86,6 +86,8 @@
 		"openRouterProvider": "ओपनराउटर",
 		"openRouterApiKeyLabel": "ओपनराउटर एपीआई कुंजी",
 		"openRouterApiKeyPlaceholder": "अपनी ओपनराउटर एपीआई कुंजी दर्ज करें",
+		"openRouterProviderRoutingLabel": "OpenRouter प्रदाता रूटिंग",
+		"openRouterProviderRoutingDescription": "OpenRouter आपके एम्बेडिंग मॉडल के लिए सर्वोत्तम उपलब्ध प्रदाताओं को अनुरोध भेजता है। डिफ़ॉल्ट रूप से, अपटाइम को अधिकतम करने के लिए अनुरोधों को शीर्ष प्रदाताओं के बीच संतुलित किया जाता है। हालांकि, आप इस मॉडल के लिए उपयोग करने के लिए एक विशिष्ट प्रदाता चुन सकते हैं।",
 		"mistralProvider": "Mistral",
 		"mistralApiKeyLabel": "API कुंजी:",
 		"mistralApiKeyPlaceholder": "अपनी मिस्ट्रल एपीआई कुंजी दर्ज करें",

+ 2 - 0
webview-ui/src/i18n/locales/id/settings.json

@@ -86,6 +86,8 @@
 		"openRouterProvider": "OpenRouter",
 		"openRouterApiKeyLabel": "Kunci API OpenRouter",
 		"openRouterApiKeyPlaceholder": "Masukkan kunci API OpenRouter Anda",
+		"openRouterProviderRoutingLabel": "Perutean Provider OpenRouter",
+		"openRouterProviderRoutingDescription": "OpenRouter mengarahkan permintaan ke provider terbaik yang tersedia untuk model embedding Anda. Secara default, permintaan diseimbangkan beban di seluruh provider teratas untuk memaksimalkan uptime. Namun, Anda dapat memilih provider spesifik untuk digunakan untuk model ini.",
 		"mistralProvider": "Mistral",
 		"mistralApiKeyLabel": "Kunci API:",
 		"mistralApiKeyPlaceholder": "Masukkan kunci API Mistral Anda",

+ 2 - 0
webview-ui/src/i18n/locales/it/settings.json

@@ -86,6 +86,8 @@
 		"openRouterProvider": "OpenRouter",
 		"openRouterApiKeyLabel": "Chiave API OpenRouter",
 		"openRouterApiKeyPlaceholder": "Inserisci la tua chiave API OpenRouter",
+		"openRouterProviderRoutingLabel": "Routing dei fornitori OpenRouter",
+		"openRouterProviderRoutingDescription": "OpenRouter indirizza le richieste ai migliori fornitori disponibili per il tuo modello di embedding. Per impostazione predefinita, le richieste sono bilanciate tra i principali fornitori per massimizzare il tempo di attività. Tuttavia, puoi scegliere un fornitore specifico da utilizzare per questo modello.",
 		"mistralProvider": "Mistral",
 		"mistralApiKeyLabel": "Chiave API:",
 		"mistralApiKeyPlaceholder": "Inserisci la tua chiave API Mistral",

+ 2 - 0
webview-ui/src/i18n/locales/ja/settings.json

@@ -86,6 +86,8 @@
 		"openRouterProvider": "OpenRouter",
 		"openRouterApiKeyLabel": "OpenRouter APIキー",
 		"openRouterApiKeyPlaceholder": "OpenRouter APIキーを入力してください",
+		"openRouterProviderRoutingLabel": "OpenRouterプロバイダールーティング",
+		"openRouterProviderRoutingDescription": "OpenRouterは、埋め込みモデルに最適な利用可能なプロバイダーにリクエストをルーティングします。デフォルトでは、稼働時間を最大化するために、リクエストはトッププロバイダー間で負荷分散されます。ただし、このモデルに使用する特定のプロバイダーを選択することもできます。",
 		"mistralProvider": "Mistral",
 		"mistralApiKeyLabel": "APIキー:",
 		"mistralApiKeyPlaceholder": "Mistral APIキーを入力してください",

+ 2 - 0
webview-ui/src/i18n/locales/ko/settings.json

@@ -89,6 +89,8 @@
 		"openRouterProvider": "OpenRouter",
 		"openRouterApiKeyLabel": "OpenRouter API 키",
 		"openRouterApiKeyPlaceholder": "OpenRouter API 키를 입력하세요",
+		"openRouterProviderRoutingLabel": "OpenRouter 공급자 라우팅",
+		"openRouterProviderRoutingDescription": "OpenRouter는 임베딩 모델에 가장 적합한 공급자로 요청을 라우팅합니다. 기본적으로 요청은 가동 시간을 최대화하기 위해 상위 공급자 간에 로드 밸런싱됩니다. 그러나 이 모델에 사용할 특정 공급자를 선택할 수 있습니다.",
 		"openaiCompatibleProvider": "OpenAI 호환",
 		"openAiKeyLabel": "OpenAI API 키",
 		"openAiKeyPlaceholder": "OpenAI API 키를 입력하세요",

+ 2 - 0
webview-ui/src/i18n/locales/nl/settings.json

@@ -86,6 +86,8 @@
 		"openRouterProvider": "OpenRouter",
 		"openRouterApiKeyLabel": "OpenRouter API-sleutel",
 		"openRouterApiKeyPlaceholder": "Voer uw OpenRouter API-sleutel in",
+		"openRouterProviderRoutingLabel": "OpenRouter Provider Routing",
+		"openRouterProviderRoutingDescription": "OpenRouter stuurt verzoeken naar de best beschikbare providers voor uw embedding model. Standaard worden verzoeken verdeeld over de beste providers om de uptime te maximaliseren. U kunt echter een specifieke provider kiezen om voor dit model te gebruiken.",
 		"mistralProvider": "Mistral",
 		"mistralApiKeyLabel": "API-sleutel:",
 		"mistralApiKeyPlaceholder": "Voer uw Mistral API-sleutel in",

+ 2 - 0
webview-ui/src/i18n/locales/pl/settings.json

@@ -89,6 +89,8 @@
 		"openRouterProvider": "OpenRouter",
 		"openRouterApiKeyLabel": "Klucz API OpenRouter",
 		"openRouterApiKeyPlaceholder": "Wprowadź swój klucz API OpenRouter",
+		"openRouterProviderRoutingLabel": "Routing dostawców OpenRouter",
+		"openRouterProviderRoutingDescription": "OpenRouter kieruje żądania do najlepszych dostępnych dostawców dla Twojego modelu osadzania. Domyślnie żądania są równoważone między najlepszymi dostawcami, aby zmaksymalizować czas działania. Możesz jednak wybrać konkretnego dostawcę do użycia z tym modelem.",
 		"openaiCompatibleProvider": "Kompatybilny z OpenAI",
 		"openAiKeyLabel": "Klucz API OpenAI",
 		"openAiKeyPlaceholder": "Wprowadź swój klucz API OpenAI",

+ 2 - 0
webview-ui/src/i18n/locales/pt-BR/settings.json

@@ -86,6 +86,8 @@
 		"openRouterProvider": "OpenRouter",
 		"openRouterApiKeyLabel": "Chave de API do OpenRouter",
 		"openRouterApiKeyPlaceholder": "Digite sua chave de API do OpenRouter",
+		"openRouterProviderRoutingLabel": "Roteamento de Provedores OpenRouter",
+		"openRouterProviderRoutingDescription": "OpenRouter direciona solicitações para os melhores provedores disponíveis para seu modelo de embedding. Por padrão, as solicitações são balanceadas entre os principais provedores para maximizar o tempo de atividade. No entanto, você pode escolher um provedor específico para usar com este modelo.",
 		"mistralProvider": "Mistral",
 		"mistralApiKeyLabel": "Chave de API:",
 		"mistralApiKeyPlaceholder": "Digite sua chave de API da Mistral",

+ 2 - 0
webview-ui/src/i18n/locales/ru/settings.json

@@ -86,6 +86,8 @@
 		"openRouterProvider": "OpenRouter",
 		"openRouterApiKeyLabel": "Ключ API OpenRouter",
 		"openRouterApiKeyPlaceholder": "Введите свой ключ API OpenRouter",
+		"openRouterProviderRoutingLabel": "Маршрутизация провайдера OpenRouter",
+		"openRouterProviderRoutingDescription": "OpenRouter направляет запросы к лучшим доступным провайдерам для вашей модели эмбеддинга. По умолчанию запросы балансируются между топовыми провайдерами для максимальной доступности. Однако вы можете выбрать конкретного провайдера для этой модели.",
 		"mistralProvider": "Mistral",
 		"mistralApiKeyLabel": "Ключ API:",
 		"mistralApiKeyPlaceholder": "Введите свой API-ключ Mistral",

+ 2 - 0
webview-ui/src/i18n/locales/tr/settings.json

@@ -89,6 +89,8 @@
 		"openRouterProvider": "OpenRouter",
 		"openRouterApiKeyLabel": "OpenRouter API Anahtarı",
 		"openRouterApiKeyPlaceholder": "OpenRouter API anahtarınızı girin",
+		"openRouterProviderRoutingLabel": "OpenRouter Sağlayıcı Yönlendirmesi",
+		"openRouterProviderRoutingDescription": "OpenRouter, gömme modeliniz için mevcut en iyi sağlayıcılara istekleri yönlendirir. Varsayılan olarak, istekler çalışma süresini en üst düzeye çıkarmak için en iyi sağlayıcılar arasında dengelenir. Ancak, bu model için kullanılacak belirli bir sağlayıcı seçebilirsiniz.",
 		"openaiCompatibleProvider": "OpenAI Uyumlu",
 		"openAiKeyLabel": "OpenAI API Anahtarı",
 		"openAiKeyPlaceholder": "OpenAI API anahtarınızı girin",

+ 2 - 0
webview-ui/src/i18n/locales/vi/settings.json

@@ -89,6 +89,8 @@
 		"openRouterProvider": "OpenRouter",
 		"openRouterApiKeyLabel": "Khóa API OpenRouter",
 		"openRouterApiKeyPlaceholder": "Nhập khóa API OpenRouter của bạn",
+		"openRouterProviderRoutingLabel": "Định tuyến nhà cung cấp OpenRouter",
+		"openRouterProviderRoutingDescription": "OpenRouter chuyển hướng yêu cầu đến các nhà cung cấp tốt nhất hiện có cho mô hình nhúng của bạn. Theo mặc định, các yêu cầu được cân bằng giữa các nhà cung cấp hàng đầu để tối đa hóa thời gian hoạt động. Tuy nhiên, bạn có thể chọn một nhà cung cấp cụ thể để sử dụng cho mô hình này.",
 		"openaiCompatibleProvider": "Tương thích OpenAI",
 		"openAiKeyLabel": "Khóa API OpenAI",
 		"openAiKeyPlaceholder": "Nhập khóa API OpenAI của bạn",

+ 2 - 0
webview-ui/src/i18n/locales/zh-CN/settings.json

@@ -91,6 +91,8 @@
 		"openRouterProvider": "OpenRouter",
 		"openRouterApiKeyLabel": "OpenRouter API 密钥",
 		"openRouterApiKeyPlaceholder": "输入您的 OpenRouter API 密钥",
+		"openRouterProviderRoutingLabel": "OpenRouter 提供商路由",
+		"openRouterProviderRoutingDescription": "OpenRouter 将请求路由到适合您嵌入模型的最佳可用提供商。默认情况下,请求会在顶级提供商之间进行负载均衡以最大化正常运行时间。但是,您可以为此模型选择特定的提供商。",
 		"mistralProvider": "Mistral",
 		"mistralApiKeyLabel": "API 密钥:",
 		"mistralApiKeyPlaceholder": "输入您的 Mistral API 密钥",

+ 2 - 0
webview-ui/src/i18n/locales/zh-TW/settings.json

@@ -86,6 +86,8 @@
 		"openRouterProvider": "OpenRouter",
 		"openRouterApiKeyLabel": "OpenRouter API 金鑰",
 		"openRouterApiKeyPlaceholder": "輸入您的 OpenRouter API 金鑰",
+		"openRouterProviderRoutingLabel": "OpenRouter 供應商路由",
+		"openRouterProviderRoutingDescription": "OpenRouter 會將請求路由到適合您嵌入模型的最佳可用供應商。預設情況下,請求會在頂尖供應商之間進行負載平衡以最大化正常運作時間。您也可以為此模型選擇特定的供應商。",
 		"mistralProvider": "Mistral",
 		"mistralApiKeyLabel": "API 金鑰:",
 		"mistralApiKeyPlaceholder": "輸入您的 Mistral API 金鑰",

+ 15 - 1
webview-ui/src/utils/test-utils.tsx

@@ -1,5 +1,6 @@
 import React from "react"
 import { render, RenderOptions } from "@testing-library/react"
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
 
 import { TooltipProvider } from "@src/components/ui/tooltip"
 import { STANDARD_TOOLTIP_DELAY } from "@src/components/ui/standard-tooltip"
@@ -9,7 +10,20 @@ interface AllTheProvidersProps {
 }
 
 const AllTheProviders = ({ children }: AllTheProvidersProps) => {
-	return <TooltipProvider delayDuration={STANDARD_TOOLTIP_DELAY}>{children}</TooltipProvider>
+	// Create a new QueryClient for each test to avoid state leakage
+	const queryClient = new QueryClient({
+		defaultOptions: {
+			queries: {
+				retry: false, // Disable retries in tests
+			},
+		},
+	})
+
+	return (
+		<QueryClientProvider client={queryClient}>
+			<TooltipProvider delayDuration={STANDARD_TOOLTIP_DELAY}>{children}</TooltipProvider>
+		</QueryClientProvider>
+	)
 }
 
 const customRender = (ui: React.ReactElement, options?: Omit<RenderOptions, "wrapper">) =>