Skip to content

Commit eb74f02

Browse files
authored
Choose specific provider when using OpenRouter (#1753)
* Choose specific provider when using OpenRouter * Add translations
1 parent a17be07 commit eb74f02

27 files changed

+379
-23
lines changed

.changeset/lovely-jeans-worry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": minor
3+
---
4+
5+
API provider: Choose specific provider when using OpenRouter

src/api/providers/openrouter.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { getModelParams, SingleCompletionHandler } from ".."
1515
import { BaseProvider } from "./base-provider"
1616
import { defaultHeaders } from "./openai"
1717

18+
const OPENROUTER_DEFAULT_PROVIDER_NAME = "[default]"
19+
1820
// Add custom interface for OpenRouter params.
1921
type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
2022
transforms?: string[]
@@ -109,6 +111,11 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
109111
messages: openAiMessages,
110112
stream: true,
111113
include_reasoning: true,
114+
// Only include provider if openRouterSpecificProvider is not "[default]".
115+
...(this.options.openRouterSpecificProvider &&
116+
this.options.openRouterSpecificProvider !== OPENROUTER_DEFAULT_PROVIDER_NAME && {
117+
provider: { order: [this.options.openRouterSpecificProvider] },
118+
}),
112119
// This way, the transforms field will only be included in the parameters when openRouterUseMiddleOutTransform is true.
113120
...((this.options.openRouterUseMiddleOutTransform ?? true) && { transforms: ["middle-out"] }),
114121
}

src/exports/roo-code.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ export type GlobalStateKey =
203203
| "openRouterModelId"
204204
| "openRouterModelInfo"
205205
| "openRouterBaseUrl"
206+
| "openRouterSpecificProvider"
206207
| "openRouterUseMiddleOutTransform"
207208
| "googleGeminiBaseUrl"
208209
| "allowedCommands"

src/shared/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface ApiHandlerOptions {
3030
openRouterModelId?: string
3131
openRouterModelInfo?: ModelInfo
3232
openRouterBaseUrl?: string
33+
openRouterSpecificProvider?: string
3334
awsAccessKey?: string
3435
awsSecretKey?: string
3536
awsSessionToken?: string
@@ -97,6 +98,7 @@ export const API_CONFIG_KEYS: GlobalStateKey[] = [
9798
"openRouterModelId",
9899
"openRouterModelInfo",
99100
"openRouterBaseUrl",
101+
"openRouterSpecificProvider",
100102
"awsRegion",
101103
"awsUseCrossRegionInference",
102104
// "awsUsePromptCache", // NOT exist on GlobalStateKey

src/shared/globalState.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export const GLOBAL_STATE_KEYS = [
7171
"openRouterModelId",
7272
"openRouterModelInfo",
7373
"openRouterBaseUrl",
74+
"openRouterSpecificProvider",
7475
"openRouterUseMiddleOutTransform",
7576
"googleGeminiBaseUrl",
7677
"allowedCommands",

webview-ui/package-lock.json

Lines changed: 38 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@radix-ui/react-slot": "^1.1.2",
3030
"@radix-ui/react-tooltip": "^1.1.8",
3131
"@tailwindcss/vite": "^4.0.0",
32+
"@tanstack/react-query": "^5.68.0",
3233
"@vscode/webview-ui-toolkit": "^1.4.0",
3334
"class-variance-authority": "^0.7.1",
3435
"clsx": "^2.1.1",
@@ -56,7 +57,8 @@
5657
"tailwind-merge": "^2.6.0",
5758
"tailwindcss": "^4.0.0",
5859
"tailwindcss-animate": "^1.0.7",
59-
"vscrui": "^0.2.2"
60+
"vscrui": "^0.2.2",
61+
"zod": "^3.24.2"
6062
},
6163
"devDependencies": {
6264
"@storybook/addon-essentials": "^8.5.6",

webview-ui/src/App.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { useCallback, useEffect, useRef, useState } from "react"
22
import { useEvent } from "react-use"
3+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
4+
35
import { ExtensionMessage } from "../../src/shared/ExtensionMessage"
46
import TranslationProvider from "./i18n/TranslationContext"
57

@@ -16,12 +18,6 @@ import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog"
1618

1719
type Tab = "settings" | "history" | "mcp" | "prompts" | "chat"
1820

19-
type HumanRelayDialogState = {
20-
isOpen: boolean
21-
requestId: string
22-
promptText: string
23-
}
24-
2521
const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]>, Tab>> = {
2622
chatButtonClicked: "chat",
2723
settingsButtonClicked: "settings",
@@ -36,7 +32,12 @@ const App = () => {
3632

3733
const [showAnnouncement, setShowAnnouncement] = useState(false)
3834
const [tab, setTab] = useState<Tab>("chat")
39-
const [humanRelayDialogState, setHumanRelayDialogState] = useState<HumanRelayDialogState>({
35+
36+
const [humanRelayDialogState, setHumanRelayDialogState] = useState<{
37+
isOpen: boolean
38+
requestId: string
39+
promptText: string
40+
}>({
4041
isOpen: false,
4142
requestId: "",
4243
promptText: "",
@@ -122,10 +123,14 @@ const App = () => {
122123
)
123124
}
124125

126+
const queryClient = new QueryClient()
127+
125128
const AppWithProviders = () => (
126129
<ExtensionStateContextProvider>
127130
<TranslationProvider>
128-
<App />
131+
<QueryClientProvider client={queryClient}>
132+
<App />
133+
</QueryClientProvider>
129134
</TranslationProvider>
130135
</ExtensionStateContextProvider>
131136
)

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useDebounce, useEvent } from "react-use"
55
import { Checkbox, Dropdown, type DropdownOption } from "vscrui"
66
import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
77
import * as vscodemodels from "vscode"
8+
import { ExternalLinkIcon } from "@radix-ui/react-icons"
89

910
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, Button } from "@/components/ui"
1011

@@ -38,7 +39,12 @@ import {
3839
} from "../../../../src/shared/api"
3940
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
4041

41-
import { vscode } from "../../utils/vscode"
42+
import { vscode } from "@/utils/vscode"
43+
import {
44+
useOpenRouterModelProviders,
45+
OPENROUTER_DEFAULT_PROVIDER_NAME,
46+
} from "@/components/ui/hooks/useOpenRouterModelProviders"
47+
4248
import { VSCodeButtonLink } from "../common/VSCodeButtonLink"
4349
import { ModelInfoView } from "./ModelInfoView"
4450
import { ModelPicker } from "./ModelPicker"
@@ -94,6 +100,7 @@ const ApiOptions = ({
94100
setErrorMessage,
95101
}: ApiOptionsProps) => {
96102
const { t } = useAppTranslation()
103+
97104
const [ollamaModels, setOllamaModels] = useState<string[]>([])
98105
const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
99106
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
@@ -192,6 +199,13 @@ const ApiOptions = ({
192199
setErrorMessage(apiValidationResult)
193200
}, [apiConfiguration, glamaModels, openRouterModels, setErrorMessage, unboundModels, requestyModels])
194201

202+
const { data: openRouterModelProviders } = useOpenRouterModelProviders(apiConfiguration?.openRouterModelId, {
203+
enabled:
204+
selectedProvider === "openrouter" &&
205+
!!apiConfiguration?.openRouterModelId &&
206+
apiConfiguration.openRouterModelId in openRouterModels,
207+
})
208+
195209
const onMessage = useCallback((event: MessageEvent) => {
196210
const message: ExtensionMessage = event.data
197211

@@ -1365,6 +1379,52 @@ const ApiOptions = ({
13651379
/>
13661380
)}
13671381

1382+
{openRouterModelProviders && (
1383+
<>
1384+
<div className="dropdown-container" style={{ marginTop: 3 }}>
1385+
<div className="flex items-center gap-1">
1386+
<label htmlFor="provider-routing" className="font-medium">
1387+
{t("settings:providers.openRouter.providerRouting.title")}
1388+
</label>
1389+
<a href={`https://openrouter.ai/${selectedModelId}/providers`}>
1390+
<ExternalLinkIcon className="w-4 h-4" />
1391+
</a>
1392+
</div>
1393+
<Dropdown
1394+
id="provider-routing"
1395+
value={apiConfiguration?.openRouterSpecificProvider || ""}
1396+
onChange={(event) => {
1397+
const provider = typeof event == "string" ? event : event?.value
1398+
const providerModelInfo = provider ? openRouterModelProviders[provider] : undefined
1399+
1400+
if (providerModelInfo) {
1401+
setApiConfigurationField("openRouterModelInfo", {
1402+
...apiConfiguration.openRouterModelInfo,
1403+
...providerModelInfo,
1404+
})
1405+
}
1406+
1407+
setApiConfigurationField("openRouterSpecificProvider", provider)
1408+
}}
1409+
options={[
1410+
{ value: OPENROUTER_DEFAULT_PROVIDER_NAME, label: OPENROUTER_DEFAULT_PROVIDER_NAME },
1411+
...Object.entries(openRouterModelProviders).map(([value, { label }]) => ({
1412+
value,
1413+
label,
1414+
})),
1415+
]}
1416+
className="w-full"
1417+
/>
1418+
</div>
1419+
<div className="text-sm text-vscode-descriptionForeground">
1420+
{t("settings:providers.openRouter.providerRouting.description")}{" "}
1421+
<a href="https://openrouter.ai/docs/features/provider-routing">
1422+
{t("settings:providers.openRouter.providerRouting.learnMore")}.
1423+
</a>
1424+
</div>
1425+
</>
1426+
)}
1427+
13681428
{selectedProvider === "glama" && (
13691429
<ModelPicker
13701430
apiConfiguration={apiConfiguration}

webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// npx jest src/components/settings/__tests__/ApiOptions.test.ts
22

33
import { render, screen } from "@testing-library/react"
4+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
5+
6+
import { ExtensionStateContextProvider } from "@/context/ExtensionStateContext"
47

5-
import { ExtensionStateContextProvider } from "../../../context/ExtensionStateContext"
68
import ApiOptions from "../ApiOptions"
79

810
// Mock VSCode components
@@ -85,10 +87,12 @@ jest.mock("../ThinkingBudget", () => ({
8587
) : null,
8688
}))
8789

88-
describe("ApiOptions", () => {
89-
const renderApiOptions = (props = {}) => {
90-
render(
91-
<ExtensionStateContextProvider>
90+
const renderApiOptions = (props = {}) => {
91+
const queryClient = new QueryClient()
92+
93+
render(
94+
<ExtensionStateContextProvider>
95+
<QueryClientProvider client={queryClient}>
9296
<ApiOptions
9397
errorMessage={undefined}
9498
setErrorMessage={() => {}}
@@ -97,10 +101,12 @@ describe("ApiOptions", () => {
97101
setApiConfigurationField={() => {}}
98102
{...props}
99103
/>
100-
</ExtensionStateContextProvider>,
101-
)
102-
}
104+
</QueryClientProvider>
105+
</ExtensionStateContextProvider>,
106+
)
107+
}
103108

109+
describe("ApiOptions", () => {
104110
it("shows temperature control by default", () => {
105111
renderApiOptions()
106112
expect(screen.getByTestId("temperature-control")).toBeInTheDocument()

0 commit comments

Comments
 (0)