Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ node_modules
coverage/

.DS_Store
.history/

# Builds
bin/
Expand Down
8 changes: 8 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,12 @@ 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 !== "" &&
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
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 @@ -93,6 +94,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 @@ -56,6 +56,7 @@ export const GLOBAL_STATE_KEYS = [
"openRouterModelId",
"openRouterModelInfo",
"openRouterBaseUrl",
"openRouterSpecificProvider",
"openRouterUseMiddleOutTransform",
"allowedCommands",
"soundEnabled",
Expand Down
86 changes: 86 additions & 0 deletions webview-ui/src/components/settings/ApiOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { TemperatureControl } from "./TemperatureControl"
import { validateApiConfiguration, validateModelId } from "@/utils/validate"
import { ApiErrorMessage } from "./ApiErrorMessage"
import { ThinkingBudget } from "./ThinkingBudget"
import { getOpenRouterProvidersForModel, OPENROUTER_DEFAULT_PROVIDER_NAME } from "../../utils/openrouter-helper"

const modelsByProvider: Record<string, Record<string, ModelInfo>> = {
anthropic: anthropicModels,
Expand Down Expand Up @@ -91,6 +92,7 @@ const ApiOptions = ({
errorMessage,
setErrorMessage,
}: ApiOptionsProps) => {
const [openRouterProviders, setOpenRouterProviders] = useState<Record<string, ModelInfo>>({})
const [ollamaModels, setOllamaModels] = useState<string[]>([])
const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
Expand Down Expand Up @@ -186,6 +188,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) {
setApiConfigurationField("openRouterModelInfo", targetModelInfo)
}
}
}, [
selectedProvider,
apiConfiguration.openRouterSpecificProvider,
openRouterProviders,
openRouterModels,
selectedModelId,
apiConfiguration.openRouterModelInfo,
setApiConfigurationField,
])

const onMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data

Expand Down Expand Up @@ -1213,6 +1262,43 @@ const ApiOptions = ({
/>
)}

{selectedProvider === "openrouter" && (
<>
<div className="dropdown-container" style={{ marginTop: 3 }}>
<label htmlFor="openrouter-specific-provider" className="font-medium">
Provider (more infos on{" "}
<a href={`https://openrouter.ai/${selectedModelId}/providers`}>OpenRouter</a>)
</label>
<Dropdown
id="openrouter-specific-provider"
value={apiConfiguration?.openRouterSpecificProvider || ""}
onChange={handleInputChange("openRouterSpecificProvider", dropdownEventTransform)}
style={{ width: "100%" }}
options={[
{ value: OPENROUTER_DEFAULT_PROVIDER_NAME, label: OPENROUTER_DEFAULT_PROVIDER_NAME },
// Add provider options from the openRouterProviders state with more info
...Object.keys(openRouterProviders).map((provider) => {
const providerInfo = openRouterProviders[provider]
const contextWindow = providerInfo?.contextWindow
? `${Math.floor(providerInfo.contextWindow / 1000)}K`
: "?"
const inputPrice = providerInfo?.inputPrice
? `$${providerInfo.inputPrice.toFixed(2)}`
: "?"
const outputPrice = providerInfo?.outputPrice
? `$${providerInfo.outputPrice.toFixed(2)}`
: "?"
return {
value: provider,
label: `${provider} (Context: ${contextWindow}, Price: ${inputPrice} / ${outputPrice})`,
}
}),
]}
/>
</div>
</>
)}

{selectedProvider === "glama" && (
<ModelPicker
apiConfiguration={apiConfiguration}
Expand Down
56 changes: 56 additions & 0 deletions webview-ui/src/utils/openrouter-helper.ts
Original file line number Diff line number Diff line change
@@ -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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can DRY things up a here a bit with the getOpenRouterModels function, which does a similar decoration of the model info. It probably also makes sense for getOpenRouterModels to live in this helper file. What do you think? I'm happy to take a stab at that later today as well.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first I wanted to include the getOpenRouterProvidersForModel function into the \src\api\providers\openrouter.ts file. But since there are no others import from "below webview-ui", I supposed it would be a bad idea.

const models: Record<string, ModelInfo> = {}

try {
const response = await axios.get(`https://openrouter.ai/api/v1/models/${modelId}/endpoints`)
const rawEndpoints = response.data.data

for (const rawEndpoint of rawEndpoints.endpoints) {
const modelInfo: ModelInfo = {
maxTokens: rawEndpoint.max_completion_tokens,
contextWindow: rawEndpoint.context_length,
supportsImages: rawEndpoints.architecture?.modality?.includes("image"),
supportsPromptCache: false,
inputPrice: parseApiPrice(rawEndpoint.pricing?.prompt),
outputPrice: parseApiPrice(rawEndpoint.pricing?.completion),
description: rawEndpoints.description,
thinking: modelId === "anthropic/claude-3.7-sonnet:thinking",
}

// Set additional properties based on model type
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 = rawEndpoint.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
// Add other cases as needed
default:
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 0.3
modelInfo.cacheReadsPrice = 0.03
break
}

const providerName = rawEndpoint.name.split("|")[0].trim()
models[providerName] = modelInfo
}
} catch (error) {
console.error(`Error fetching OpenRouter providers:`, error)
}

return models
}