Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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">
{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}
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