Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lovely-jeans-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"roo-cline": minor
---

API provider: Choose specific provider when using OpenRouter
7 changes: 7 additions & 0 deletions src/api/providers/openrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down Expand Up @@ -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"] }),
}
Expand Down
1 change: 1 addition & 0 deletions src/exports/roo-code.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ export type GlobalStateKey =
| "openRouterModelId"
| "openRouterModelInfo"
| "openRouterBaseUrl"
| "openRouterSpecificProvider"
| "openRouterUseMiddleOutTransform"
| "googleGeminiBaseUrl"
| "allowedCommands"
Expand Down
2 changes: 2 additions & 0 deletions src/shared/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface ApiHandlerOptions {
openRouterModelId?: string
openRouterModelInfo?: ModelInfo
openRouterBaseUrl?: string
openRouterSpecificProvider?: string
awsAccessKey?: string
awsSecretKey?: string
awsSessionToken?: string
Expand Down Expand Up @@ -97,6 +98,7 @@ export const API_CONFIG_KEYS: GlobalStateKey[] = [
"openRouterModelId",
"openRouterModelInfo",
"openRouterBaseUrl",
"openRouterSpecificProvider",
"awsRegion",
"awsUseCrossRegionInference",
// "awsUsePromptCache", // NOT exist on GlobalStateKey
Expand Down
1 change: 1 addition & 0 deletions src/shared/globalState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const GLOBAL_STATE_KEYS = [
"openRouterModelId",
"openRouterModelInfo",
"openRouterBaseUrl",
"openRouterSpecificProvider",
"openRouterUseMiddleOutTransform",
"googleGeminiBaseUrl",
"allowedCommands",
Expand Down
39 changes: 38 additions & 1 deletion webview-ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion webview-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
21 changes: 13 additions & 8 deletions webview-ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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",
Expand All @@ -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: "",
Expand Down Expand Up @@ -122,10 +123,14 @@ const App = () => {
)
}

const queryClient = new QueryClient()

const AppWithProviders = () => (
<ExtensionStateContextProvider>
<TranslationProvider>
<App />
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</TranslationProvider>
</ExtensionStateContextProvider>
)
Expand Down
62 changes: 61 additions & 1 deletion webview-ui/src/components/settings/ApiOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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[]>([])
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
are load balanced across the top providers to maximize uptime. However, you can choose a
specific provider to use for this model. Learn more about provider routing{" "}
<a href="https://openrouter.ai/docs/features/provider-routing">here</a>.
</div>
</>
)}

{selectedProvider === "glama" && (
<ModelPicker
apiConfiguration={apiConfiguration}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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={() => {}}
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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", () => ({
Expand Down Expand Up @@ -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 }
}

Expand Down
Loading
Loading