Repository: RooVetGit/Roo-Code
Pull request #1753: Choose specific provider when using OpenRouter
Author: login: cte
Labels: name: Enhancement
description: New feature or request
name: size:L
description: This PR changes 100-499 lines, ignoring generated files.
name: lgtm
description: This PR has been approved by a maintainer
Created: 2025-03-17T20:47:04Z | Merged: 2025-03-17T22:40:27Z

## Context

<img width="569" alt="Screenshot 2025-03-17 at 1 59 28 PM" src="https://github.com/user-attachments/assets/3c86770e-2a36-49d7-ac68-bdd0a937b928" />

## Implementation

<!--

Some description of HOW you achieved it. Perhaps give a high level description of the program flow. Did you need to refactor something? What tradeoffs did you take? Are there things in here which you’d particularly like people to pay close attention to?

-->

## Screenshots

| before | after |
| ------ | ----- |
|        |       |

## How to Test

<!--

A straightforward scenario of how to test your changes will help reviewers that are not familiar with the part of the code that you are changing but want to see it in action. This section can include a description or step-by-step instructions of how to get to the state of v2 that your change affects.

A "How To Test" section can look something like this:

- Sign in with a user with tracks
- Activate `show_awesome_cat_gifs` feature (add `?feature.show_awesome_cat_gifs=1` to your URL)
- You should see a GIF with cats dancing

-->

## Get in Touch

<!-- We'd love to have a way to chat with you about your changes if necessary. If you're in the [Roo Code Discord](https://discord.gg/roocode), please share your handle here. -->

<!-- ELLIPSIS_HIDDEN -->


----

> [!IMPORTANT]
> Add feature to select specific providers for OpenRouter with UI support and translations.
> 
>   - **Behavior**:
>     - Add ability to choose specific provider for OpenRouter in `openrouter.ts`.
>     - Default provider set to `[default]` if no specific provider is chosen.
>   - **UI Components**:
>     - Add provider selection dropdown in `ApiOptions.tsx`.
>     - Use `useOpenRouterModelProviders` hook in `ApiOptions.tsx` for fetching providers.
>     - Integrate `QueryClientProvider` in `App.tsx` and related test files.
>   - **Hooks**:
>     - New hook `useOpenRouterModelProviders` in `useOpenRouterModelProviders.ts` for fetching provider data.
>   - **Translations**:
>     - Add translations for provider routing in multiple language files including `en`, `es`, `fr`, `hi`, `it`, `ja`, `ko`, `pl`, `pt-BR`, `tr`, `vi`, `zh-CN`, `zh-TW`.
> 
> <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=RooVetGit%2FRoo-Code&utm_source=github&utm_medium=referral)<sup> for ff9792dca12e60e3154f57d65d50629c1f1a45e1. It will automatically update as commits are pushed.</sup>


<!-- ELLIPSIS_HIDDEN -->


Diff:
diff --git a/.changeset/lovely-jeans-worry.md b/.changeset/lovely-jeans-worry.md
new file mode 100644
index 00000000000..9ed8af3a1f3
--- /dev/null
+++ b/.changeset/lovely-jeans-worry.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": minor
+---
+
+API provider: Choose specific provider when using OpenRouter
diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts
index d681c42ec3d..3823400455d 100644
--- a/src/api/providers/openrouter.ts
+++ b/src/api/providers/openrouter.ts
@@ -15,6 +15,8 @@ import { getModelParams, SingleCompletionHandler } from ".."
 import { BaseProvider } from "./base-provider"
 import { defaultHeaders } from "./openai"
 
+const OPENROUTER_DEFAULT_PROVIDER_NAME = "[default]"
+
 // Add custom interface for OpenRouter params.
 type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
 	transforms?: string[]
@@ -109,6 +111,11 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
 			messages: openAiMessages,
 			stream: true,
 			include_reasoning: true,
+			// Only include provider if openRouterSpecificProvider is not "[default]".
+			...(this.options.openRouterSpecificProvider &&
+				this.options.openRouterSpecificProvider !== OPENROUTER_DEFAULT_PROVIDER_NAME && {
+					provider: { order: [this.options.openRouterSpecificProvider] },
+				}),
 			// This way, the transforms field will only be included in the parameters when openRouterUseMiddleOutTransform is true.
 			...((this.options.openRouterUseMiddleOutTransform ?? true) && { transforms: ["middle-out"] }),
 		}
diff --git a/src/exports/roo-code.d.ts b/src/exports/roo-code.d.ts
index 13e6c0979c7..ed54f564661 100644
--- a/src/exports/roo-code.d.ts
+++ b/src/exports/roo-code.d.ts
@@ -203,6 +203,7 @@ export type GlobalStateKey =
 	| "openRouterModelId"
 	| "openRouterModelInfo"
 	| "openRouterBaseUrl"
+	| "openRouterSpecificProvider"
 	| "openRouterUseMiddleOutTransform"
 	| "googleGeminiBaseUrl"
 	| "allowedCommands"
diff --git a/src/shared/api.ts b/src/shared/api.ts
index 624db186125..1bc432157b7 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -30,6 +30,7 @@ export interface ApiHandlerOptions {
 	openRouterModelId?: string
 	openRouterModelInfo?: ModelInfo
 	openRouterBaseUrl?: string
+	openRouterSpecificProvider?: string
 	awsAccessKey?: string
 	awsSecretKey?: string
 	awsSessionToken?: string
@@ -97,6 +98,7 @@ export const API_CONFIG_KEYS: GlobalStateKey[] = [
 	"openRouterModelId",
 	"openRouterModelInfo",
 	"openRouterBaseUrl",
+	"openRouterSpecificProvider",
 	"awsRegion",
 	"awsUseCrossRegionInference",
 	// "awsUsePromptCache", // NOT exist on GlobalStateKey
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index c2b78f1ef79..9175bf22eec 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -71,6 +71,7 @@ export const GLOBAL_STATE_KEYS = [
 	"openRouterModelId",
 	"openRouterModelInfo",
 	"openRouterBaseUrl",
+	"openRouterSpecificProvider",
 	"openRouterUseMiddleOutTransform",
 	"googleGeminiBaseUrl",
 	"allowedCommands",
diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json
index 439e5bd7975..3b491a7fddf 100644
--- a/webview-ui/package-lock.json
+++ b/webview-ui/package-lock.json
@@ -21,6 +21,7 @@
 				"@radix-ui/react-slot": "^1.1.2",
 				"@radix-ui/react-tooltip": "^1.1.8",
 				"@tailwindcss/vite": "^4.0.0",
+				"@tanstack/react-query": "^5.68.0",
 				"@vscode/webview-ui-toolkit": "^1.4.0",
 				"class-variance-authority": "^0.7.1",
 				"clsx": "^2.1.1",
@@ -48,7 +49,8 @@
 				"tailwind-merge": "^2.6.0",
 				"tailwindcss": "^4.0.0",
 				"tailwindcss-animate": "^1.0.7",
-				"vscrui": "^0.2.2"
+				"vscrui": "^0.2.2",
+				"zod": "^3.24.2"
 			},
 			"devDependencies": {
 				"@storybook/addon-essentials": "^8.5.6",
@@ -6533,6 +6535,32 @@
 				"vite": "^5.2.0 || ^6"
 			}
 		},
+		"node_modules/@tanstack/query-core": {
+			"version": "5.68.0",
+			"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.68.0.tgz",
+			"integrity": "sha512-r8rFYYo8/sY/LNaOqX84h12w7EQev4abFXDWy4UoDVUJzJ5d9Fbmb8ayTi7ScG+V0ap44SF3vNs/45mkzDGyGw==",
+			"license": "MIT",
+			"funding": {
+				"type": "github",
+				"url": "https://github.com/sponsors/tannerlinsley"
+			}
+		},
+		"node_modules/@tanstack/react-query": {
+			"version": "5.68.0",
+			"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.68.0.tgz",
+			"integrity": "sha512-mMOdGDKlwTP/WV72QqSNf4PAMeoBp/DqBHQ222wBfb51Looi8QUqnCnb9O98ZgvNISmy6fzxRGBJdZ+9IBvX2Q==",
+			"license": "MIT",
+			"dependencies": {
+				"@tanstack/query-core": "5.68.0"
+			},
+			"funding": {
+				"type": "github",
+				"url": "https://github.com/sponsors/tannerlinsley"
+			},
+			"peerDependencies": {
+				"react": "^18 || ^19"
+			}
+		},
 		"node_modules/@testing-library/dom": {
 			"version": "10.4.0",
 			"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
@@ -21967,6 +21995,15 @@
 				"url": "https://github.com/sponsors/sindresorhus"
 			}
 		},
+		"node_modules/zod": {
+			"version": "3.24.2",
+			"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
+			"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
+			"license": "MIT",
+			"funding": {
+				"url": "https://github.com/sponsors/colinhacks"
+			}
+		},
 		"node_modules/zwitch": {
 			"version": "2.0.4",
 			"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
diff --git a/webview-ui/package.json b/webview-ui/package.json
index 2f9c8eb002a..ac0bb935ff0 100644
--- a/webview-ui/package.json
+++ b/webview-ui/package.json
@@ -29,6 +29,7 @@
 		"@radix-ui/react-slot": "^1.1.2",
 		"@radix-ui/react-tooltip": "^1.1.8",
 		"@tailwindcss/vite": "^4.0.0",
+		"@tanstack/react-query": "^5.68.0",
 		"@vscode/webview-ui-toolkit": "^1.4.0",
 		"class-variance-authority": "^0.7.1",
 		"clsx": "^2.1.1",
@@ -56,7 +57,8 @@
 		"tailwind-merge": "^2.6.0",
 		"tailwindcss": "^4.0.0",
 		"tailwindcss-animate": "^1.0.7",
-		"vscrui": "^0.2.2"
+		"vscrui": "^0.2.2",
+		"zod": "^3.24.2"
 	},
 	"devDependencies": {
 		"@storybook/addon-essentials": "^8.5.6",
diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx
index 4276d404cd2..59a40472518 100644
--- a/webview-ui/src/App.tsx
+++ b/webview-ui/src/App.tsx
@@ -1,5 +1,7 @@
 import { useCallback, useEffect, useRef, useState } from "react"
 import { useEvent } from "react-use"
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
+
 import { ExtensionMessage } from "../../src/shared/ExtensionMessage"
 import TranslationProvider from "./i18n/TranslationContext"
 
@@ -16,12 +18,6 @@ import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog"
 
 type Tab = "settings" | "history" | "mcp" | "prompts" | "chat"
 
-type HumanRelayDialogState = {
-	isOpen: boolean
-	requestId: string
-	promptText: string
-}
-
 const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]>, Tab>> = {
 	chatButtonClicked: "chat",
 	settingsButtonClicked: "settings",
@@ -36,7 +32,12 @@ const App = () => {
 
 	const [showAnnouncement, setShowAnnouncement] = useState(false)
 	const [tab, setTab] = useState<Tab>("chat")
-	const [humanRelayDialogState, setHumanRelayDialogState] = useState<HumanRelayDialogState>({
+
+	const [humanRelayDialogState, setHumanRelayDialogState] = useState<{
+		isOpen: boolean
+		requestId: string
+		promptText: string
+	}>({
 		isOpen: false,
 		requestId: "",
 		promptText: "",
@@ -122,10 +123,14 @@ const App = () => {
 	)
 }
 
+const queryClient = new QueryClient()
+
 const AppWithProviders = () => (
 	<ExtensionStateContextProvider>
 		<TranslationProvider>
-			<App />
+			<QueryClientProvider client={queryClient}>
+				<App />
+			</QueryClientProvider>
 		</TranslationProvider>
 	</ExtensionStateContextProvider>
 )
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index 02cd1c93752..8ea5f8ab45e 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -5,6 +5,7 @@ import { useDebounce, useEvent } from "react-use"
 import { Checkbox, Dropdown, type DropdownOption } from "vscrui"
 import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 import * as vscodemodels from "vscode"
+import { ExternalLinkIcon } from "@radix-ui/react-icons"
 
 import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, Button } from "@/components/ui"
 
@@ -38,7 +39,12 @@ import {
 } from "../../../../src/shared/api"
 import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
 
-import { vscode } from "../../utils/vscode"
+import { vscode } from "@/utils/vscode"
+import {
+	useOpenRouterModelProviders,
+	OPENROUTER_DEFAULT_PROVIDER_NAME,
+} from "@/components/ui/hooks/useOpenRouterModelProviders"
+
 import { VSCodeButtonLink } from "../common/VSCodeButtonLink"
 import { ModelInfoView } from "./ModelInfoView"
 import { ModelPicker } from "./ModelPicker"
@@ -94,6 +100,7 @@ const ApiOptions = ({
 	setErrorMessage,
 }: ApiOptionsProps) => {
 	const { t } = useAppTranslation()
+
 	const [ollamaModels, setOllamaModels] = useState<string[]>([])
 	const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
 	const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
@@ -192,6 +199,13 @@ const ApiOptions = ({
 		setErrorMessage(apiValidationResult)
 	}, [apiConfiguration, glamaModels, openRouterModels, setErrorMessage, unboundModels, requestyModels])
 
+	const { data: openRouterModelProviders } = useOpenRouterModelProviders(apiConfiguration?.openRouterModelId, {
+		enabled:
+			selectedProvider === "openrouter" &&
+			!!apiConfiguration?.openRouterModelId &&
+			apiConfiguration.openRouterModelId in openRouterModels,
+	})
+
 	const onMessage = useCallback((event: MessageEvent) => {
 		const message: ExtensionMessage = event.data
 
@@ -1365,6 +1379,52 @@ const ApiOptions = ({
 				/>
 			)}
 
+			{openRouterModelProviders && (
+				<>
+					<div className="dropdown-container" style={{ marginTop: 3 }}>
+						<div className="flex items-center gap-1">
+							<label htmlFor="provider-routing" className="font-medium">
+								{t("settings:providers.openRouter.providerRouting.title")}
+							</label>
+							<a href={`https://openrouter.ai/${selectedModelId}/providers`}>
+								<ExternalLinkIcon className="w-4 h-4" />
+							</a>
+						</div>
+						<Dropdown
+							id="provider-routing"
+							value={apiConfiguration?.openRouterSpecificProvider || ""}
+							onChange={(event) => {
+								const provider = typeof event == "string" ? event : event?.value
+								const providerModelInfo = provider ? openRouterModelProviders[provider] : undefined
+
+								if (providerModelInfo) {
+									setApiConfigurationField("openRouterModelInfo", {
+										...apiConfiguration.openRouterModelInfo,
+										...providerModelInfo,
+									})
+								}
+
+								setApiConfigurationField("openRouterSpecificProvider", provider)
+							}}
+							options={[
+								{ value: OPENROUTER_DEFAULT_PROVIDER_NAME, label: OPENROUTER_DEFAULT_PROVIDER_NAME },
+								...Object.entries(openRouterModelProviders).map(([value, { label }]) => ({
+									value,
+									label,
+								})),
+							]}
+							className="w-full"
+						/>
+					</div>
+					<div className="text-sm text-vscode-descriptionForeground">
+						{t("settings:providers.openRouter.providerRouting.description")}{" "}
+						<a href="https://openrouter.ai/docs/features/provider-routing">
+							{t("settings:providers.openRouter.providerRouting.learnMore")}.
+						</a>
+					</div>
+				</>
+			)}
+
 			{selectedProvider === "glama" && (
 				<ModelPicker
 					apiConfiguration={apiConfiguration}
diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
index 008c5819db8..65bff4ce791 100644
--- a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
+++ b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
@@ -1,8 +1,10 @@
 // npx jest src/components/settings/__tests__/ApiOptions.test.ts
 
 import { render, screen } from "@testing-library/react"
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
+
+import { ExtensionStateContextProvider } from "@/context/ExtensionStateContext"
 
-import { ExtensionStateContextProvider } from "../../../context/ExtensionStateContext"
 import ApiOptions from "../ApiOptions"
 
 // Mock VSCode components
@@ -85,10 +87,12 @@ jest.mock("../ThinkingBudget", () => ({
 		) : null,
 }))
 
-describe("ApiOptions", () => {
-	const renderApiOptions = (props = {}) => {
-		render(
-			<ExtensionStateContextProvider>
+const renderApiOptions = (props = {}) => {
+	const queryClient = new QueryClient()
+
+	render(
+		<ExtensionStateContextProvider>
+			<QueryClientProvider client={queryClient}>
 				<ApiOptions
 					errorMessage={undefined}
 					setErrorMessage={() => {}}
@@ -97,10 +101,12 @@ describe("ApiOptions", () => {
 					setApiConfigurationField={() => {}}
 					{...props}
 				/>
-			</ExtensionStateContextProvider>,
-		)
-	}
+			</QueryClientProvider>
+		</ExtensionStateContextProvider>,
+	)
+}
 
+describe("ApiOptions", () => {
 	it("shows temperature control by default", () => {
 		renderApiOptions()
 		expect(screen.getByTestId("temperature-control")).toBeInTheDocument()
diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx
index 95b3bb8fa55..3add06154d3 100644
--- a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx
+++ b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx
@@ -1,9 +1,12 @@
 // npx jest src/components/settings/__tests__/SettingsView.test.ts
 
 import { render, screen, fireEvent } from "@testing-library/react"
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
+
+import { vscode } from "@/utils/vscode"
+import { ExtensionStateContextProvider } from "@/context/ExtensionStateContext"
+
 import SettingsView from "../SettingsView"
-import { ExtensionStateContextProvider } from "../../../context/ExtensionStateContext"
-import { vscode } from "../../../utils/vscode"
 
 // Mock vscode API
 jest.mock("../../../utils/vscode", () => ({
@@ -124,13 +127,19 @@ const mockPostMessage = (state: any) => {
 
 const renderSettingsView = () => {
 	const onDone = jest.fn()
+	const queryClient = new QueryClient()
+
 	render(
 		<ExtensionStateContextProvider>
-			<SettingsView onDone={onDone} />
+			<QueryClientProvider client={queryClient}>
+				<SettingsView onDone={onDone} />
+			</QueryClientProvider>
 		</ExtensionStateContextProvider>,
 	)
-	// Hydrate initial state
+
+	// Hydrate initial state.
 	mockPostMessage({})
+
 	return { onDone }
 }
 
diff --git a/webview-ui/src/components/ui/hooks/useOpenRouterModelProviders.ts b/webview-ui/src/components/ui/hooks/useOpenRouterModelProviders.ts
new file mode 100644
index 00000000000..b8111d5d35f
--- /dev/null
+++ b/webview-ui/src/components/ui/hooks/useOpenRouterModelProviders.ts
@@ -0,0 +1,116 @@
+import axios from "axios"
+import { z } from "zod"
+import { useQuery, UseQueryOptions } from "@tanstack/react-query"
+
+import { ModelInfo } from "../../../../../src/shared/api"
+import { parseApiPrice } from "../../../../../src/utils/cost"
+
+export const OPENROUTER_DEFAULT_PROVIDER_NAME = "[default]"
+
+const openRouterEndpointsSchema = z.object({
+	data: z.object({
+		id: z.string(),
+		name: z.string(),
+		description: z.string().optional(),
+		architecture: z
+			.object({
+				modality: z.string().optional(),
+				tokenizer: z.string().optional(),
+			})
+			.optional(),
+		endpoints: z.array(
+			z.object({
+				name: z.string(),
+				context_length: z.number(),
+				max_completion_tokens: z.number().optional(),
+				pricing: z
+					.object({
+						prompt: z.union([z.string(), z.number()]).optional(),
+						completion: z.union([z.string(), z.number()]).optional(),
+					})
+					.optional(),
+			}),
+		),
+	}),
+})
+
+type OpenRouterModelProvider = ModelInfo & {
+	label: string
+}
+
+async function getOpenRouterProvidersForModel(modelId: string) {
+	const models: Record<string, OpenRouterModelProvider> = {}
+
+	try {
+		const response = await axios.get(`https://openrouter.ai/api/v1/models/${modelId}/endpoints`)
+		const result = openRouterEndpointsSchema.safeParse(response.data)
+
+		if (!result.success) {
+			console.error("OpenRouter API response validation failed:", result.error)
+			return models
+		}
+
+		const { id, description, architecture, endpoints } = result.data.data
+
+		for (const endpoint of endpoints) {
+			const providerName = endpoint.name.split("|")[0].trim()
+			const inputPrice = parseApiPrice(endpoint.pricing?.prompt)
+			const outputPrice = parseApiPrice(endpoint.pricing?.completion)
+
+			const modelInfo: OpenRouterModelProvider = {
+				maxTokens: endpoint.max_completion_tokens,
+				contextWindow: endpoint.context_length,
+				supportsImages: architecture?.modality?.includes("image"),
+				supportsPromptCache: false,
+				inputPrice,
+				outputPrice,
+				description,
+				thinking: modelId === "anthropic/claude-3.7-sonnet:thinking",
+				label: providerName,
+			}
+
+			switch (true) {
+				case modelId.startsWith("anthropic/claude-3.7-sonnet"):
+					modelInfo.supportsComputerUse = true
+					modelInfo.supportsPromptCache = true
+					modelInfo.cacheWritesPrice = 3.75
+					modelInfo.cacheReadsPrice = 0.3
+					modelInfo.maxTokens = id === "anthropic/claude-3.7-sonnet:thinking" ? 64_000 : 16_384
+					break
+				case modelId.startsWith("anthropic/claude-3.5-sonnet-20240620"):
+					modelInfo.supportsPromptCache = true
+					modelInfo.cacheWritesPrice = 3.75
+					modelInfo.cacheReadsPrice = 0.3
+					modelInfo.maxTokens = 8192
+					break
+				default:
+					modelInfo.supportsPromptCache = true
+					modelInfo.cacheWritesPrice = 0.3
+					modelInfo.cacheReadsPrice = 0.03
+					break
+			}
+
+			models[providerName] = modelInfo
+		}
+	} catch (error) {
+		if (error instanceof z.ZodError) {
+			console.error(`OpenRouter API response validation failed:`, error.errors)
+		} else {
+			console.error(`Error fetching OpenRouter providers:`, error)
+		}
+	}
+
+	return models
+}
+
+type UseOpenRouterModelProvidersOptions = Omit<
+	UseQueryOptions<Record<string, OpenRouterModelProvider>>,
+	"queryKey" | "queryFn"
+>
+
+export const useOpenRouterModelProviders = (modelId?: string, options?: UseOpenRouterModelProvidersOptions) =>
+	useQuery<Record<string, OpenRouterModelProvider>>({
+		queryKey: ["openrouter-model-providers", modelId],
+		queryFn: () => (modelId ? getOpenRouterProvidersForModel(modelId) : {}),
+		...options,
+	})
diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json
index 8ddd8d01c52..d93e69670e6 100644
--- a/webview-ui/src/i18n/locales/ca/settings.json
+++ b/webview-ui/src/i18n/locales/ca/settings.json
@@ -152,6 +152,13 @@
 			"description": "No es requereix clau API, però l'usuari necessita ajuda per copiar i enganxar informació al xat d'IA web.",
 			"instructions": "Durant l'ús, apareixerà un diàleg i el missatge actual es copiarà automàticament al porta-retalls. Necessiteu enganxar-lo a les versions web d'IA (com ChatGPT o Claude), després copiar la resposta de l'IA de nou al diàleg i fer clic al botó de confirmació."
 		},
+		"openRouter": {
+			"providerRouting": {
+				"title": "Encaminament de Proveïdors d'OpenRouter",
+				"description": "OpenRouter dirigeix les sol·licituds als millors proveïdors disponibles per al vostre model. 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.",
+				"learnMore": "Més informació sobre l'encaminament de proveïdors"
+			}
+		},
 		"customModel": {
 			"capabilities": "Configureu les capacitats i preus per al vostre model personalitzat compatible amb OpenAI. Tingueu cura en especificar les capacitats del model, ja que poden afectar com funciona Roo Code.",
 			"maxTokens": {
diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json
index 8dc76333021..34b81c678a0 100644
--- a/webview-ui/src/i18n/locales/de/settings.json
+++ b/webview-ui/src/i18n/locales/de/settings.json
@@ -152,6 +152,13 @@
 			"description": "Es ist kein API-Schlüssel erforderlich, aber der Benutzer muss beim Kopieren und Einfügen der Informationen in den Web-Chat-KI helfen.",
 			"instructions": "Während der Verwendung wird ein Dialogfeld angezeigt und die aktuelle Nachricht wird automatisch in die Zwischenablage kopiert. Sie müssen diese in Web-Versionen von KI (wie ChatGPT oder Claude) einfügen, dann die Antwort der KI zurück in das Dialogfeld kopieren und auf die Bestätigungsschaltfläche klicken."
 		},
+		"openRouter": {
+			"providerRouting": {
+				"title": "OpenRouter Anbieter-Routing",
+				"description": "OpenRouter leitet Anfragen an die besten verfügbaren Anbieter für Ihr Modell weiter. Standardmäßig werden Anfragen über die Top-Anbieter lastverteilt, um maximale Verfügbarkeit zu gewährleisten. Sie können jedoch einen bestimmten Anbieter für dieses Modell auswählen.",
+				"learnMore": "Mehr über Anbieter-Routing erfahren"
+			}
+		},
 		"customModel": {
 			"capabilities": "Konfigurieren Sie die Fähigkeiten und Preise für Ihr benutzerdefiniertes OpenAI-kompatibles Modell. Seien Sie vorsichtig bei der Angabe der Modellfähigkeiten, da diese beeinflussen können, wie Roo Code funktioniert.",
 			"maxTokens": {
diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json
index 3d3ae02a582..c73b144eece 100644
--- a/webview-ui/src/i18n/locales/en/settings.json
+++ b/webview-ui/src/i18n/locales/en/settings.json
@@ -152,6 +152,13 @@
 			"description": "No API key is required, but the user needs to help copy and paste the information to the web chat AI.",
 			"instructions": "During use, a dialog box will pop up and the current message will be copied to the clipboard automatically. You need to paste these to web versions of AI (such as ChatGPT or Claude), then copy the AI's reply back to the dialog box and click the confirm button."
 		},
+		"openRouter": {
+			"providerRouting": {
+				"title": "OpenRouter Provider Routing",
+				"description": "OpenRouter routes requests to the best available providers for your 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.",
+				"learnMore": "Learn more about provider routing"
+			}
+		},
 		"customModel": {
 			"capabilities": "Configure the capabilities and pricing for your custom OpenAI-compatible model. Be careful when specifying the model capabilities, as they can affect how Roo Code performs.",
 			"maxTokens": {
diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json
index e28b4289211..91e2cdfdde5 100644
--- a/webview-ui/src/i18n/locales/es/settings.json
+++ b/webview-ui/src/i18n/locales/es/settings.json
@@ -152,6 +152,13 @@
 			"description": "No se requiere clave API, pero el usuario necesita ayudar a copiar y pegar la información en el chat web de IA.",
 			"instructions": "Durante el uso, aparecerá un cuadro de diálogo y el mensaje actual se copiará automáticamente al portapapeles. Debe pegarlo en las versiones web de IA (como ChatGPT o Claude), luego copiar la respuesta de la IA de vuelta al cuadro de diálogo y hacer clic en el botón de confirmar."
 		},
+		"openRouter": {
+			"providerRouting": {
+				"title": "Enrutamiento de Proveedores de OpenRouter",
+				"description": "OpenRouter dirige las solicitudes a los mejores proveedores disponibles para su modelo. 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.",
+				"learnMore": "Más información sobre el enrutamiento de proveedores"
+			}
+		},
 		"customModel": {
 			"capabilities": "Configure las capacidades y precios para su modelo personalizado compatible con OpenAI. Tenga cuidado al especificar las capacidades del modelo, ya que pueden afectar cómo funciona Roo Code.",
 			"maxTokens": {
diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json
index 07e4264677a..cd957897811 100644
--- a/webview-ui/src/i18n/locales/fr/settings.json
+++ b/webview-ui/src/i18n/locales/fr/settings.json
@@ -152,6 +152,13 @@
 			"description": "Aucune clé API n'est requise, mais l'utilisateur doit aider à copier et coller les informations dans le chat web de l'IA.",
 			"instructions": "Pendant l'utilisation, une boîte de dialogue apparaîtra et le message actuel sera automatiquement copié dans le presse-papiers. Vous devez le coller dans les versions web de l'IA (comme ChatGPT ou Claude), puis copier la réponse de l'IA dans la boîte de dialogue et cliquer sur le bouton de confirmation."
 		},
+		"openRouter": {
+			"providerRouting": {
+				"title": "Routage des fournisseurs OpenRouter",
+				"description": "OpenRouter dirige les requêtes vers les meilleurs fournisseurs disponibles pour votre modèle. 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.",
+				"learnMore": "En savoir plus sur le routage des fournisseurs"
+			}
+		},
 		"customModel": {
 			"capabilities": "Configurez les capacités et les prix pour votre modèle personnalisé compatible OpenAI. Soyez prudent lors de la spécification des capacités du modèle, car elles peuvent affecter le fonctionnement de Roo Code.",
 			"maxTokens": {
diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json
index 639628dd078..b05778af1dc 100644
--- a/webview-ui/src/i18n/locales/hi/settings.json
+++ b/webview-ui/src/i18n/locales/hi/settings.json
@@ -152,6 +152,13 @@
 			"description": "कोई API कुंजी आवश्यक नहीं है, लेकिन उपयोगकर्ता को वेब चैट AI में जानकारी कॉपी और पेस्ट करने में मदद करनी होगी।",
 			"instructions": "उपयोग के दौरान, एक डायलॉग बॉक्स पॉप अप होगा और वर्तमान संदेश स्वचालित रूप से क्लिपबोर्ड पर कॉपी हो जाएगा। आपको इन्हें AI के वेब संस्करणों (जैसे ChatGPT या Claude) में पेस्ट करना होगा, फिर AI की प्रतिक्रिया को डायलॉग बॉक्स में वापस कॉपी करें और पुष्टि बटन पर क्लिक करें।"
 		},
+		"openRouter": {
+			"providerRouting": {
+				"title": "OpenRouter प्रदाता रूटिंग",
+				"description": "OpenRouter आपके मॉडल के लिए सर्वोत्तम उपलब्ध प्रदाताओं को अनुरोध भेजता है। डिफ़ॉल्ट रूप से, अपटाइम को अधिकतम करने के लिए अनुरोधों को शीर्ष प्रदाताओं के बीच संतुलित किया जाता है। हालांकि, आप इस मॉडल के लिए उपयोग करने के लिए एक विशिष्ट प्रदाता चुन सकते हैं।",
+				"learnMore": "प्रदाता रूटिंग के बारे में अधिक जानें"
+			}
+		},
 		"customModel": {
 			"capabilities": "अपने कस्टम OpenAI-संगत मॉडल के लिए क्षमताओं और मूल्य निर्धारण को कॉन्फ़िगर करें। मॉडल क्षमताओं को निर्दिष्ट करते समय सावधान रहें, क्योंकि वे Roo Code के प्रदर्शन को प्रभावित कर सकती हैं।",
 			"maxTokens": {
diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json
index d1d0ffb0ee9..37b5b8583d3 100644
--- a/webview-ui/src/i18n/locales/it/settings.json
+++ b/webview-ui/src/i18n/locales/it/settings.json
@@ -152,6 +152,13 @@
 			"description": "Non è richiesta alcuna chiave API, ma l'utente dovrà aiutare a copiare e incollare le informazioni nella chat web AI.",
 			"instructions": "Durante l'uso, apparirà una finestra di dialogo e il messaggio corrente verrà automaticamente copiato negli appunti. Dovrai incollarlo nelle versioni web dell'AI (come ChatGPT o Claude), quindi copiare la risposta dell'AI nella finestra di dialogo e fare clic sul pulsante di conferma."
 		},
+		"openRouter": {
+			"providerRouting": {
+				"title": "Routing dei fornitori OpenRouter",
+				"description": "OpenRouter indirizza le richieste ai migliori fornitori disponibili per il tuo modello. 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.",
+				"learnMore": "Scopri di più sul routing dei fornitori"
+			}
+		},
 		"customModel": {
 			"capabilities": "Configura le capacità e i prezzi del tuo modello personalizzato compatibile con OpenAI. Fai attenzione quando specifichi le capacità del modello, poiché possono influenzare le prestazioni di Roo Code.",
 			"maxTokens": {
diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json
index 17ce51c7e3f..14e93195f39 100644
--- a/webview-ui/src/i18n/locales/ja/settings.json
+++ b/webview-ui/src/i18n/locales/ja/settings.json
@@ -152,6 +152,13 @@
 			"description": "APIキーは不要ですが、ユーザーはウェブチャットAIに情報をコピー＆ペーストする必要があります。",
 			"instructions": "使用中にダイアログボックスが表示され、現在のメッセージが自動的にクリップボードにコピーされます。これらをウェブ版のAI（ChatGPTやClaudeなど）に貼り付け、AIの返答をダイアログボックスにコピーして確認ボタンをクリックする必要があります。"
 		},
+		"openRouter": {
+			"providerRouting": {
+				"title": "OpenRouterプロバイダールーティング",
+				"description": "OpenRouterはあなたのモデルに最適な利用可能なプロバイダーにリクエストを転送します。デフォルトでは、稼働時間を最大化するために、リクエストはトッププロバイダー間でロードバランスされます。ただし、このモデルに使用する特定のプロバイダーを選択することもできます。",
+				"learnMore": "プロバイダールーティングについて詳しく知る"
+			}
+		},
 		"customModel": {
 			"capabilities": "カスタムOpenAI互換モデルの機能と価格を設定します。モデルの機能はRoo Codeのパフォーマンスに影響を与える可能性があるため、慎重に指定してください。",
 			"maxTokens": {
diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json
index 7ea2082d189..369dd62a1a6 100644
--- a/webview-ui/src/i18n/locales/ko/settings.json
+++ b/webview-ui/src/i18n/locales/ko/settings.json
@@ -152,6 +152,13 @@
 			"description": "API 키가 필요하지 않지만, 사용자가 웹 채팅 AI에 정보를 복사하여 붙여넣어야 합니다.",
 			"instructions": "사용 중에 대화 상자가 나타나고 현재 메시지가 자동으로 클립보드에 복사됩니다. 이를 웹 버전 AI(예: ChatGPT 또는 Claude)에 붙여넣은 다음, AI의 응답을 대화 상자에 복사하고 확인 버튼을 클릭해야 합니다."
 		},
+		"openRouter": {
+			"providerRouting": {
+				"title": "OpenRouter 제공자 라우팅",
+				"description": "OpenRouter는 귀하의 모델에 가장 적합한 사용 가능한 제공자에게 요청을 전달합니다. 기본적으로 요청은 가동 시간을 최대화하기 위해 상위 제공자 간에 부하 분산됩니다. 그러나 이 모델에 사용할 특정 제공자를 선택할 수 있습니다.",
+				"learnMore": "제공자 라우팅에 대해 자세히 알아보기"
+			}
+		},
 		"customModel": {
 			"capabilities": "사용자 정의 OpenAI 호환 모델의 기능과 가격을 구성하세요. 모델 기능이 Roo Code의 성능에 영향을 미칠 수 있으므로 신중하게 지정하세요.",
 			"maxTokens": {
diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json
index 3534915c988..22c7d41fec8 100644
--- a/webview-ui/src/i18n/locales/pl/settings.json
+++ b/webview-ui/src/i18n/locales/pl/settings.json
@@ -152,6 +152,13 @@
 			"description": "Nie jest wymagany klucz API, ale użytkownik będzie musiał pomóc w kopiowaniu i wklejaniu informacji do czatu internetowego AI.",
 			"instructions": "Podczas użytkowania pojawi się okno dialogowe, a bieżąca wiadomość zostanie automatycznie skopiowana do schowka. Będziesz musiał wkleić ją do internetowych wersji AI (takich jak ChatGPT lub Claude), a następnie skopiować odpowiedź AI z powrotem do okna dialogowego i kliknąć przycisk potwierdzenia."
 		},
+		"openRouter": {
+			"providerRouting": {
+				"title": "Routing dostawców OpenRouter",
+				"description": "OpenRouter kieruje żądania do najlepszych dostępnych dostawców dla Twojego modelu. 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.",
+				"learnMore": "Dowiedz się więcej o routingu dostawców"
+			}
+		},
 		"customModel": {
 			"capabilities": "Skonfiguruj możliwości i ceny swojego niestandardowego modelu zgodnego z OpenAI. Zachowaj ostrożność podczas określania możliwości modelu, ponieważ mogą one wpływać na wydajność Roo Code.",
 			"maxTokens": {
diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json
index 677e005e3ff..14ec9ee4802 100644
--- a/webview-ui/src/i18n/locales/pt-BR/settings.json
+++ b/webview-ui/src/i18n/locales/pt-BR/settings.json
@@ -152,6 +152,13 @@
 			"description": "Não é necessária chave de API, mas o usuário precisa ajudar a copiar e colar as informações para a IA do chat web.",
 			"instructions": "Durante o uso, uma caixa de diálogo será exibida e a mensagem atual será copiada para a área de transferência automaticamente. Você precisa colar isso nas versões web de IA (como ChatGPT ou Claude), depois copiar a resposta da IA de volta para a caixa de diálogo e clicar no botão confirmar."
 		},
+		"openRouter": {
+			"providerRouting": {
+				"title": "Roteamento de Provedores OpenRouter",
+				"description": "OpenRouter direciona solicitações para os melhores provedores disponíveis para seu modelo. 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.",
+				"learnMore": "Saiba mais sobre roteamento de provedores"
+			}
+		},
 		"customModel": {
 			"capabilities": "Configure as capacidades e preços para seu modelo personalizado compatível com OpenAI. Tenha cuidado ao especificar as capacidades do modelo, pois elas podem afetar como o Roo Code funciona.",
 			"maxTokens": {
diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json
index a1babd31560..9478b03e89e 100644
--- a/webview-ui/src/i18n/locales/tr/settings.json
+++ b/webview-ui/src/i18n/locales/tr/settings.json
@@ -152,6 +152,13 @@
 			"description": "API anahtarı gerekmez, ancak kullanıcının bilgileri web sohbet yapay zekasına kopyalayıp yapıştırması gerekir.",
 			"instructions": "Kullanım sırasında bir iletişim kutusu açılacak ve mevcut mesaj otomatik olarak panoya kopyalanacaktır. Bunları web yapay zekalarına (ChatGPT veya Claude gibi) yapıştırmanız, ardından yapay zekanın yanıtını iletişim kutusuna kopyalayıp onay düğmesine tıklamanız gerekir."
 		},
+		"openRouter": {
+			"providerRouting": {
+				"title": "OpenRouter Sağlayıcı Yönlendirmesi",
+				"description": "OpenRouter, 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.",
+				"learnMore": "Sağlayıcı yönlendirmesi hakkında daha fazla bilgi edinin"
+			}
+		},
 		"customModel": {
 			"capabilities": "Özel OpenAI uyumlu modelinizin yeteneklerini ve fiyatlandırmasını yapılandırın. Model yeteneklerini belirtirken dikkatli olun, çünkü bunlar Roo Code'un performansını etkileyebilir.",
 			"maxTokens": {
diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json
index 5a56bbc7cce..103d3ace664 100644
--- a/webview-ui/src/i18n/locales/vi/settings.json
+++ b/webview-ui/src/i18n/locales/vi/settings.json
@@ -152,6 +152,13 @@
 			"description": "Không cần khóa API, nhưng người dùng cần giúp sao chép và dán thông tin vào AI trò chuyện web.",
 			"instructions": "Trong quá trình sử dụng, một hộp thoại sẽ xuất hiện và tin nhắn hiện tại sẽ được tự động sao chép vào clipboard. Bạn cần dán chúng vào các phiên bản web của AI (như ChatGPT hoặc Claude), sau đó sao chép phản hồi của AI trở lại hộp thoại và nhấp vào nút xác nhận."
 		},
+		"openRouter": {
+			"providerRouting": {
+				"title": "Định tuyến nhà cung cấp OpenRouter",
+				"description": "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 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.",
+				"learnMore": "Tìm hiểu thêm về định tuyến nhà cung cấp"
+			}
+		},
 		"customModel": {
 			"capabilities": "Cấu hình các khả năng và giá cả cho mô hình tương thích OpenAI tùy chỉnh của bạn. Hãy cẩn thận khi chỉ định khả năng của mô hình, vì chúng có thể ảnh hưởng đến cách Roo Code hoạt động.",
 			"maxTokens": {
diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json
index 85220108be8..4bed5509598 100644
--- a/webview-ui/src/i18n/locales/zh-CN/settings.json
+++ b/webview-ui/src/i18n/locales/zh-CN/settings.json
@@ -152,6 +152,13 @@
 			"description": "不需要 API 密钥，但用户需要帮助将信息复制并粘贴到网页聊天 AI。",
 			"instructions": "使用期间，将弹出对话框并自动将当前消息复制到剪贴板。您需要将这些内容粘贴到 AI 的网页版本（如 ChatGPT 或 Claude），然后将 AI 的回复复制回对话框并点击确认按钮。"
 		},
+		"openRouter": {
+			"providerRouting": {
+				"title": "OpenRouter 提供商路由",
+				"description": "OpenRouter 将请求路由到适合您模型的最佳可用提供商。默认情况下，请求会在顶级提供商之间进行负载均衡以最大化正常运行时间。但是，您可以为此模型选择特定的提供商。",
+				"learnMore": "了解更多关于提供商路由的信息"
+			}
+		},
 		"customModel": {
 			"capabilities": "配置您的自定义 OpenAI 兼容模型的功能和定价。在指定模型功能时要小心，因为它们会影响 Roo Code 的性能。",
 			"maxTokens": {
diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json
index a46ff308a63..7fe534bf3d2 100644
--- a/webview-ui/src/i18n/locales/zh-TW/settings.json
+++ b/webview-ui/src/i18n/locales/zh-TW/settings.json
@@ -152,6 +152,13 @@
 			"description": "不需要 API 金鑰，但使用者需要協助將資訊複製並貼上到網頁聊天 AI。",
 			"instructions": "使用期間會彈出對話框，並自動將目前訊息複製到剪貼簿。您需要將這些內容貼上到網頁版 AI（如 ChatGPT 或 Claude），然後將 AI 的回覆複製回對話框並點擊確認按鈕。"
 		},
+		"openRouter": {
+			"providerRouting": {
+				"title": "OpenRouter 提供者路由",
+				"description": "OpenRouter 將請求路由到適合您模型的最佳可用提供者。預設情況下，請求會在頂級提供者之間進行負載平衡以最大化正常運行時間。但是，您可以為此模型選擇特定的提供者。",
+				"learnMore": "了解更多關於提供者路由的資訊"
+			}
+		},
 		"customModel": {
 			"capabilities": "設定您的自訂 OpenAI 相容模型的功能和定價。請謹慎指定模型功能，因為它們會影響 Roo Code 的效能。",
 			"maxTokens": {


Reviews:
user: login: ellipsis-dev[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T20:49:47Z
user: login: ellipsis-dev[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T21:01:09Z
user: login: ellipsis-dev[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T21:59:23Z
user: login: ellipsis-dev[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T22:18:17Z
user: login: mrubens
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-17T22:18:56Z
user: login: mrubens
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T22:19:39Z
user: login: mrubens
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T22:20:23Z
user: login: cte
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T22:20:52Z
user: login: cte
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T22:34:09Z

Review comments:
diff_hunk: @@ -0,0 +1,56 @@
+import axios from "axios"
+import { ModelInfo } from "../../../src/shared/api"
+import { parseApiPrice } from "../../../src/utils/cost"
+
+export const OPENROUTER_DEFAULT_PROVIDER_NAME = "[default]"
+export async function getOpenRouterProvidersForModel(modelId: string) {
+	const models: Record<string, ModelInfo> = {}
+
+	try {
+		const response = await axios.get(`https://openrouter.ai/api/v1/models/${modelId}/endpoints`)
+		const rawEndpoints = response.data.data
path: webview-ui/src/utils/openrouter-helper.ts
user: login: ellipsis-dev[bot]
body: Consider validating that the API response structure is as expected (e.g. that `response.data.data.endpoints` exists and is an array) before iterating. This can prevent runtime errors if the API response changes.
created_at: 2025-03-17T20:49:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -109,6 +111,11 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
 			messages: openAiMessages,
 			stream: true,
 			include_reasoning: true,
+			// Only include provider if openRouterSpecificProvider is not "[default]".
+			...(this.options.openRouterSpecificProvider &&
path: src/api/providers/openrouter.ts
user: login: ellipsis-dev[bot]
body: Consider adding unit tests to verify the behavior of the conditional spread to include the 'provider' parameter based on `openRouterSpecificProvider`.
created_at: 2025-03-17T20:49:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -192,6 +195,53 @@ const ApiOptions = ({
 		setErrorMessage(apiValidationResult)
 	}, [apiConfiguration, glamaModels, openRouterModels, setErrorMessage, unboundModels, requestyModels])
 
+	useEffect(() => {
+		if (
+			selectedProvider === "openrouter" &&
+			apiConfiguration?.openRouterModelId &&
+			apiConfiguration.openRouterModelId in openRouterModels
+		) {
+			getOpenRouterProvidersForModel(apiConfiguration.openRouterModelId)
+				.then((providers) => {
+					setOpenRouterProviders(providers)
+				})
+				.catch((error) => {
+					console.error("Error fetching OpenRouter providers:", error)
+				})
+		}
+	}, [selectedProvider, apiConfiguration?.openRouterModelId, openRouterModels])
+
+	useEffect(() => {
+		if (selectedProvider === "openrouter" && apiConfiguration?.openRouterSpecificProvider) {
+			let targetModelInfo
+
+			if (apiConfiguration.openRouterSpecificProvider in openRouterProviders) {
+				const currentProviderInfo = openRouterProviders[apiConfiguration.openRouterSpecificProvider]
+				targetModelInfo = {
+					...apiConfiguration.openRouterModelInfo,
+					...currentProviderInfo,
+				}
+			} else {
+				targetModelInfo = openRouterModels[selectedModelId]
+			}
+
+			const currentModelInfoString = JSON.stringify(apiConfiguration.openRouterModelInfo)
+			const targetModelInfoString = JSON.stringify(targetModelInfo)
+
+			if (targetModelInfoString && currentModelInfoString !== targetModelInfoString) {
path: webview-ui/src/components/settings/ApiOptions.tsx
user: login: ellipsis-dev[bot]
body: Using `JSON.stringify` to compare model info objects can be error‐prone due to key order. Consider using a deep equality check utility for more robust comparison.
created_at: 2025-03-17T20:49:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1365,6 +1416,52 @@ const ApiOptions = ({
 				/>
 			)}
 
+			{selectedProvider === "openrouter" && (
+				<>
+					<div className="dropdown-container" style={{ marginTop: 3 }}>
+						<div className="flex items-center gap-1">
+							<label htmlFor="provider-routing" className="font-medium">
+								OpenRouter Provider Routing
+							</label>
+							<a href={`https://openrouter.ai/${selectedModelId}/providers`}>
path: webview-ui/src/components/settings/ApiOptions.tsx
user: login: ellipsis-dev[bot]
body: When rendering the external link for provider routing, consider adding `target="_blank"` and `rel="noopener noreferrer"` to the `<a>` tag for improved security and accessibility. (Development Standards)
```suggestion
							<a href={`https://openrouter.ai/${selectedModelId}/providers`} target="_blank" rel="noopener noreferrer">
```
created_at: 2025-03-17T21:01:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+import axios from "axios"
+import { z } from "zod"
+import { useQuery, UseQueryOptions } from "@tanstack/react-query"
+
+import { ModelInfo } from "../../../../../src/shared/api"
+import { parseApiPrice } from "../../../../../src/utils/cost"
+
+export const OPENROUTER_DEFAULT_PROVIDER_NAME = "[default]"
+
+const openRouterEndpointsSchema = z.object({
+	data: z.object({
+		id: z.string(),
+		name: z.string(),
+		description: z.string().optional(),
+		architecture: z
+			.object({
+				modality: z.string().optional(),
+				tokenizer: z.string().optional(),
+			})
+			.optional(),
+		endpoints: z.array(
+			z.object({
+				name: z.string(),
+				context_length: z.number(),
+				max_completion_tokens: z.number().optional(),
+				pricing: z
+					.object({
+						prompt: z.union([z.string(), z.number()]).optional(),
+						completion: z.union([z.string(), z.number()]).optional(),
+					})
+					.optional(),
+			}),
+		),
+	}),
+})
+
+type OpenRouterModelProvider = ModelInfo & {
+	label: string
+}
+
+async function getOpenRouterProvidersForModel(modelId: string) {
+	const models: Record<string, OpenRouterModelProvider> = {}
+
+	try {
+		const response = await axios.get(`https://openrouter.ai/api/v1/models/${modelId}/endpoints`)
+		console.log("response", response.data)
path: webview-ui/src/components/ui/hooks/useOpenRouterModelProviders.ts
user: login: ellipsis-dev[bot]
body: Remove or disable the debugging log `console.log("response", response.data)` before production deployment.
created_at: 2025-03-17T21:59:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -109,12 +111,19 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
 			messages: openAiMessages,
 			stream: true,
 			include_reasoning: true,
+			// Only include provider if openRouterSpecificProvider is not "[default]".
+			...(this.options.openRouterSpecificProvider &&
+				this.options.openRouterSpecificProvider !== OPENROUTER_DEFAULT_PROVIDER_NAME && {
+					provider: { order: [this.options.openRouterSpecificProvider] },
+				}),
 			// This way, the transforms field will only be included in the parameters when openRouterUseMiddleOutTransform is true.
 			...((this.options.openRouterUseMiddleOutTransform ?? true) && { transforms: ["middle-out"] }),
 		}
 
 		const stream = await this.client.chat.completions.create(completionParams)
 
+		console.log(`openrouter#createMessage: completionParams = ${JSON.stringify(completionParams, null, 2)}`)
path: src/api/providers/openrouter.ts
user: login: ellipsis-dev[bot]
body: Avoid leaving `console.log` debug statements in production code. Use a structured logging library with appropriate log levels, or remove the statement once debugging is complete. This follows our development standards.
```suggestion

```
created_at: 2025-03-17T22:18:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1365,6 +1379,52 @@ const ApiOptions = ({
 				/>
 			)}
 
+			{openRouterModelProviders && (
+				<>
+					<div className="dropdown-container" style={{ marginTop: 3 }}>
+						<div className="flex items-center gap-1">
+							<label htmlFor="provider-routing" className="font-medium">
+								OpenRouter Provider Routing
+							</label>
+							<a href={`https://openrouter.ai/${selectedModelId}/providers`}>
+								<ExternalLinkIcon className="w-4 h-4" />
+							</a>
+						</div>
+						<Dropdown
+							id="provider-routing"
+							value={apiConfiguration?.openRouterSpecificProvider || ""}
+							onChange={(event) => {
+								const provider = typeof event == "string" ? event : event?.value
+								const providerModelInfo = provider ? openRouterModelProviders[provider] : undefined
+
+								if (providerModelInfo) {
+									setApiConfigurationField("openRouterModelInfo", {
+										...apiConfiguration.openRouterModelInfo,
+										...providerModelInfo,
+									})
+								}
+
+								setApiConfigurationField("openRouterSpecificProvider", provider)
+							}}
+							options={[
+								{ value: OPENROUTER_DEFAULT_PROVIDER_NAME, label: OPENROUTER_DEFAULT_PROVIDER_NAME },
+								...Object.entries(openRouterModelProviders).map(([value, { label }]) => ({
+									value,
+									label,
+								})),
+							]}
+							className="w-full"
+						/>
+					</div>
+					<div className="text-sm text-vscode-descriptionForeground">
+						OpenRouter routes requests to the best available providers for your model. By default, requests
path: webview-ui/src/components/settings/ApiOptions.tsx
user: login: mrubens
body: Mind translating this?
created_at: 2025-03-17T22:19:39Z
author_association: COLLABORATOR
diff_hunk: @@ -1365,6 +1379,52 @@ const ApiOptions = ({
 				/>
 			)}
 
+			{openRouterModelProviders && (
+				<>
+					<div className="dropdown-container" style={{ marginTop: 3 }}>
+						<div className="flex items-center gap-1">
+							<label htmlFor="provider-routing" className="font-medium">
+								OpenRouter Provider Routing
+							</label>
+							<a href={`https://openrouter.ai/${selectedModelId}/providers`}>
+								<ExternalLinkIcon className="w-4 h-4" />
+							</a>
+						</div>
+						<Dropdown
+							id="provider-routing"
+							value={apiConfiguration?.openRouterSpecificProvider || ""}
+							onChange={(event) => {
+								const provider = typeof event == "string" ? event : event?.value
+								const providerModelInfo = provider ? openRouterModelProviders[provider] : undefined
+
+								if (providerModelInfo) {
+									setApiConfigurationField("openRouterModelInfo", {
+										...apiConfiguration.openRouterModelInfo,
+										...providerModelInfo,
+									})
+								}
+
+								setApiConfigurationField("openRouterSpecificProvider", provider)
+							}}
+							options={[
+								{ value: OPENROUTER_DEFAULT_PROVIDER_NAME, label: OPENROUTER_DEFAULT_PROVIDER_NAME },
+								...Object.entries(openRouterModelProviders).map(([value, { label }]) => ({
+									value,
+									label,
+								})),
+							]}
+							className="w-full"
+						/>
+					</div>
+					<div className="text-sm text-vscode-descriptionForeground">
+						OpenRouter routes requests to the best available providers for your model. By default, requests
path: webview-ui/src/components/settings/ApiOptions.tsx
user: login: mrubens
body: Or I can do it as a follow up 
created_at: 2025-03-17T22:20:23Z
author_association: COLLABORATOR
diff_hunk: @@ -1365,6 +1379,52 @@ const ApiOptions = ({
 				/>
 			)}
 
+			{openRouterModelProviders && (
+				<>
+					<div className="dropdown-container" style={{ marginTop: 3 }}>
+						<div className="flex items-center gap-1">
+							<label htmlFor="provider-routing" className="font-medium">
+								OpenRouter Provider Routing
+							</label>
+							<a href={`https://openrouter.ai/${selectedModelId}/providers`}>
+								<ExternalLinkIcon className="w-4 h-4" />
+							</a>
+						</div>
+						<Dropdown
+							id="provider-routing"
+							value={apiConfiguration?.openRouterSpecificProvider || ""}
+							onChange={(event) => {
+								const provider = typeof event == "string" ? event : event?.value
+								const providerModelInfo = provider ? openRouterModelProviders[provider] : undefined
+
+								if (providerModelInfo) {
+									setApiConfigurationField("openRouterModelInfo", {
+										...apiConfiguration.openRouterModelInfo,
+										...providerModelInfo,
+									})
+								}
+
+								setApiConfigurationField("openRouterSpecificProvider", provider)
+							}}
+							options={[
+								{ value: OPENROUTER_DEFAULT_PROVIDER_NAME, label: OPENROUTER_DEFAULT_PROVIDER_NAME },
+								...Object.entries(openRouterModelProviders).map(([value, { label }]) => ({
+									value,
+									label,
+								})),
+							]}
+							className="w-full"
+						/>
+					</div>
+					<div className="text-sm text-vscode-descriptionForeground">
+						OpenRouter routes requests to the best available providers for your model. By default, requests
path: webview-ui/src/components/settings/ApiOptions.tsx
user: login: cte
body: Oops!
created_at: 2025-03-17T22:20:52Z
author_association: COLLABORATOR
diff_hunk: @@ -1365,6 +1379,52 @@ const ApiOptions = ({
 				/>
 			)}
 
+			{openRouterModelProviders && (
+				<>
+					<div className="dropdown-container" style={{ marginTop: 3 }}>
+						<div className="flex items-center gap-1">
+							<label htmlFor="provider-routing" className="font-medium">
+								OpenRouter Provider Routing
+							</label>
+							<a href={`https://openrouter.ai/${selectedModelId}/providers`}>
+								<ExternalLinkIcon className="w-4 h-4" />
+							</a>
+						</div>
+						<Dropdown
+							id="provider-routing"
+							value={apiConfiguration?.openRouterSpecificProvider || ""}
+							onChange={(event) => {
+								const provider = typeof event == "string" ? event : event?.value
+								const providerModelInfo = provider ? openRouterModelProviders[provider] : undefined
+
+								if (providerModelInfo) {
+									setApiConfigurationField("openRouterModelInfo", {
+										...apiConfiguration.openRouterModelInfo,
+										...providerModelInfo,
+									})
+								}
+
+								setApiConfigurationField("openRouterSpecificProvider", provider)
+							}}
+							options={[
+								{ value: OPENROUTER_DEFAULT_PROVIDER_NAME, label: OPENROUTER_DEFAULT_PROVIDER_NAME },
+								...Object.entries(openRouterModelProviders).map(([value, { label }]) => ({
+									value,
+									label,
+								})),
+							]}
+							className="w-full"
+						/>
+					</div>
+					<div className="text-sm text-vscode-descriptionForeground">
+						OpenRouter routes requests to the best available providers for your model. By default, requests
path: webview-ui/src/components/settings/ApiOptions.tsx
user: login: cte
body: https://github.com/RooVetGit/Roo-Code/pull/1753/commits/ff9792dca12e60e3154f57d65d50629c1f1a45e1
created_at: 2025-03-17T22:34:09Z
author_association: COLLABORATOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-03-17T20:47:08Z
body: ###  🦋  Changeset detected

Latest commit: ff9792dca12e60e3154f57d65d50629c1f1a45e1

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name      | Type  |
| --------- | ----- |
| roo-cline | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/RooVetGit/Roo-Code/new/cte/openrouter-specific-provider?filename=.changeset/chatty-panthers-admire.md&value=---%0A%22roo-cline%22%3A%20patch%0A---%0A%0AChoose%20specific%20provider%20when%20using%20OpenRouter%0A)


author_association: NONE
user: login: cte
created_at: 2025-03-17T22:16:42Z
body: @PhunkyBob - I reworked this a bit to clean up the `useEffect` calls - hope you don't mind. Thanks for doing the heavy lifting on this!
author_association: COLLABORATOR
user: login: PhunkyBob
created_at: 2025-03-18T13:15:46Z
body: > @PhunkyBob - I reworked this a bit to clean up the `useEffect` calls - hope you don't mind. Thanks for doing the heavy lifting on this!

No problem!
I'm new to React / TypeScript (I am more a Python guy), so I don't know the best practice yet. 
author_association: NONE