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

841 lines
28 KiB
TypeScript

import React, { memo, useCallback, useEffect, useMemo, useState } from "react"
import { convertHeadersToObject } from "./utils/headers"
import { useDebounce } from "react-use"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { ExternalLinkIcon } from "@radix-ui/react-icons"
import {
type ProviderName,
type ProviderSettings,
isRetiredProvider,
DEFAULT_CONSECUTIVE_MISTAKE_LIMIT,
openRouterDefaultModelId,
requestyDefaultModelId,
litellmDefaultModelId,
openAiNativeDefaultModelId,
openAiCodexDefaultModelId,
anthropicDefaultModelId,
qwenCodeDefaultModelId,
geminiDefaultModelId,
deepSeekDefaultModelId,
moonshotDefaultModelId,
mistralDefaultModelId,
xaiDefaultModelId,
basetenDefaultModelId,
bedrockDefaultModelId,
vertexDefaultModelId,
sambaNovaDefaultModelId,
internationalZAiDefaultModelId,
mainlandZAiDefaultModelId,
fireworksDefaultModelId,
rooDefaultModelId,
vercelAiGatewayDefaultModelId,
minimaxDefaultModelId,
} from "@roo-code/types"
import {
getProviderServiceConfig,
getDefaultModelIdForProvider,
getStaticModelsForProvider,
shouldUseGenericModelPicker,
handleModelChangeSideEffects,
} from "./utils/providerModelConfig"
import { vscode } from "@src/utils/vscode"
import { validateApiConfigurationExcludingModelErrors, getModelValidationError } from "@src/utils/validate"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { useRouterModels } from "@src/components/ui/hooks/useRouterModels"
import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import {
useOpenRouterModelProviders,
OPENROUTER_DEFAULT_PROVIDER_NAME,
} from "@src/components/ui/hooks/useOpenRouterModelProviders"
import { filterProviders, filterModels } from "./utils/organizationFilters"
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
SearchableSelect,
Collapsible,
CollapsibleTrigger,
CollapsibleContent,
} from "@src/components/ui"
import {
Anthropic,
Baseten,
Bedrock,
DeepSeek,
Gemini,
LMStudio,
LiteLLM,
Mistral,
Moonshot,
Ollama,
OpenAI,
OpenAICompatible,
OpenAICodex,
OpenRouter,
QwenCode,
Requesty,
Roo,
SambaNova,
Vertex,
VSCodeLM,
XAI,
ZAi,
Fireworks,
VercelAiGateway,
MiniMax,
} from "./providers"
import { MODELS_BY_PROVIDER, PROVIDERS } from "./constants"
import { inputEventTransform, noTransform } from "./transforms"
import { ModelPicker } from "./ModelPicker"
import { ApiErrorMessage } from "./ApiErrorMessage"
import { ThinkingBudget } from "./ThinkingBudget"
import { Verbosity } from "./Verbosity"
import { TodoListSettingsControl } from "./TodoListSettingsControl"
import { TemperatureControl } from "./TemperatureControl"
import { RateLimitSecondsControl } from "./RateLimitSecondsControl"
import { ConsecutiveMistakeLimitControl } from "./ConsecutiveMistakeLimitControl"
import { BedrockCustomArn } from "./providers/BedrockCustomArn"
import { RooBalanceDisplay } from "./providers/RooBalanceDisplay"
import { buildDocLink } from "@src/utils/docLinks"
import { BookOpenText } from "lucide-react"
export interface ApiOptionsProps {
uriScheme: string | undefined
apiConfiguration: ProviderSettings
setApiConfigurationField: <K extends keyof ProviderSettings>(
field: K,
value: ProviderSettings[K],
isUserAction?: boolean,
) => void
fromWelcomeView?: boolean
errorMessage: string | undefined
setErrorMessage: React.Dispatch<React.SetStateAction<string | undefined>>
}
const ApiOptions = ({
uriScheme,
apiConfiguration,
setApiConfigurationField,
fromWelcomeView,
errorMessage,
setErrorMessage,
}: ApiOptionsProps) => {
const { t } = useAppTranslation()
const { organizationAllowList, cloudIsAuthenticated, openAiCodexIsAuthenticated } = useExtensionState()
const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => {
const headers = apiConfiguration?.openAiHeaders || {}
return Object.entries(headers)
})
useEffect(() => {
const propHeaders = apiConfiguration?.openAiHeaders || {}
if (JSON.stringify(customHeaders) !== JSON.stringify(Object.entries(propHeaders))) {
setCustomHeaders(Object.entries(propHeaders))
}
}, [apiConfiguration?.openAiHeaders, customHeaders])
// Helper to convert array of tuples to object (filtering out empty keys).
// Debounced effect to update the main configuration when local
// customHeaders state stabilizes.
useDebounce(
() => {
const currentConfigHeaders = apiConfiguration?.openAiHeaders || {}
const newHeadersObject = convertHeadersToObject(customHeaders)
// Only update if the processed object is different from the current config.
if (JSON.stringify(currentConfigHeaders) !== JSON.stringify(newHeadersObject)) {
setApiConfigurationField("openAiHeaders", newHeadersObject, false)
}
},
300,
[customHeaders, apiConfiguration?.openAiHeaders, setApiConfigurationField],
)
const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = useState(false)
const handleInputChange = useCallback(
<K extends keyof ProviderSettings, E>(
field: K,
transform: (event: E) => ProviderSettings[K] = inputEventTransform,
) =>
(event: E | Event) => {
setApiConfigurationField(field, transform(event as E))
},
[setApiConfigurationField],
)
const {
provider: selectedProvider,
id: selectedModelId,
info: selectedModelInfo,
} = useSelectedModel(apiConfiguration)
const activeSelectedProvider: ProviderName | undefined = isRetiredProvider(selectedProvider)
? undefined
: selectedProvider
const isRetiredSelectedProvider =
typeof apiConfiguration.apiProvider === "string" && isRetiredProvider(apiConfiguration.apiProvider)
const { data: routerModels, refetch: refetchRouterModels } = useRouterModels()
const { data: openRouterModelProviders } = useOpenRouterModelProviders(
apiConfiguration?.openRouterModelId,
apiConfiguration?.openRouterBaseUrl,
{
enabled:
!!apiConfiguration?.openRouterModelId &&
routerModels?.openrouter &&
Object.keys(routerModels.openrouter).length > 1 &&
apiConfiguration.openRouterModelId in routerModels.openrouter,
},
)
// Update `apiModelId` whenever `selectedModelId` changes.
useEffect(() => {
if (isRetiredSelectedProvider) {
return
}
if (selectedModelId && apiConfiguration.apiModelId !== selectedModelId) {
// Pass false as third parameter to indicate this is not a user action
// This is an internal sync, not a user-initiated change
setApiConfigurationField("apiModelId", selectedModelId, false)
}
}, [selectedModelId, setApiConfigurationField, apiConfiguration.apiModelId, isRetiredSelectedProvider])
// Debounced refresh model updates, only executed 250ms after the user
// stops typing.
useDebounce(
() => {
if (selectedProvider === "openai") {
// Use our custom headers state to build the headers object.
const headerObject = convertHeadersToObject(customHeaders)
vscode.postMessage({
type: "requestOpenAiModels",
values: {
baseUrl: apiConfiguration?.openAiBaseUrl,
apiKey: apiConfiguration?.openAiApiKey,
customHeaders: {}, // Reserved for any additional headers.
openAiHeaders: headerObject,
},
})
} else if (selectedProvider === "ollama") {
vscode.postMessage({ type: "requestOllamaModels" })
} else if (selectedProvider === "lmstudio") {
vscode.postMessage({ type: "requestLmStudioModels" })
} else if (selectedProvider === "vscode-lm") {
vscode.postMessage({ type: "requestVsCodeLmModels" })
} else if (selectedProvider === "litellm" || selectedProvider === "roo") {
vscode.postMessage({ type: "requestRouterModels" })
}
},
250,
[
selectedProvider,
apiConfiguration?.requestyApiKey,
apiConfiguration?.openAiBaseUrl,
apiConfiguration?.openAiApiKey,
apiConfiguration?.ollamaBaseUrl,
apiConfiguration?.lmStudioBaseUrl,
apiConfiguration?.litellmBaseUrl,
apiConfiguration?.litellmApiKey,
customHeaders,
],
)
useEffect(() => {
if (isRetiredSelectedProvider) {
setErrorMessage(undefined)
return
}
const apiValidationResult = validateApiConfigurationExcludingModelErrors(
apiConfiguration,
routerModels,
organizationAllowList,
)
setErrorMessage(apiValidationResult)
}, [apiConfiguration, routerModels, organizationAllowList, setErrorMessage, isRetiredSelectedProvider])
const onProviderChange = useCallback(
(value: ProviderName) => {
setApiConfigurationField("apiProvider", value)
// It would be much easier to have a single attribute that stores
// the modelId, but we have a separate attribute for each of
// OpenRouter and Requesty.
// If you switch to one of these providers and the corresponding
// modelId is not set then you immediately end up in an error state.
// To address that we set the modelId to the default value for th
// provider if it's not already set.
const validateAndResetModel = (
provider: ProviderName,
modelId: string | undefined,
field: keyof ProviderSettings,
defaultValue?: string,
) => {
// in case we haven't set a default value for a provider
if (!defaultValue) return
// 1) If nothing is set, initialize to the provider default.
if (!modelId) {
setApiConfigurationField(field, defaultValue, false)
return
}
// 2) If something *is* set, ensure it's valid for the newly selected provider.
//
// Without this, switching providers can leave the UI showing a model from the
// previously selected provider (including model IDs that don't exist for the
// newly selected provider).
//
// Note: We only validate providers with static model lists.
const staticModels = MODELS_BY_PROVIDER[provider]
if (!staticModels) {
return
}
// Bedrock has a special “custom-arn” pseudo-model that isn't part of MODELS_BY_PROVIDER.
if (provider === "bedrock" && modelId === "custom-arn") {
return
}
const filteredModels = filterModels(staticModels, provider, organizationAllowList)
const isValidModel = !!filteredModels && Object.prototype.hasOwnProperty.call(filteredModels, modelId)
if (!isValidModel) {
setApiConfigurationField(field, defaultValue, false)
}
}
// Define a mapping object that associates each provider with its model configuration
const PROVIDER_MODEL_CONFIG: Partial<
Record<
ProviderName,
{
field: keyof ProviderSettings
default?: string
}
>
> = {
openrouter: { field: "openRouterModelId", default: openRouterDefaultModelId },
requesty: { field: "requestyModelId", default: requestyDefaultModelId },
litellm: { field: "litellmModelId", default: litellmDefaultModelId },
anthropic: { field: "apiModelId", default: anthropicDefaultModelId },
"openai-codex": { field: "apiModelId", default: openAiCodexDefaultModelId },
"qwen-code": { field: "apiModelId", default: qwenCodeDefaultModelId },
"openai-native": { field: "apiModelId", default: openAiNativeDefaultModelId },
gemini: { field: "apiModelId", default: geminiDefaultModelId },
deepseek: { field: "apiModelId", default: deepSeekDefaultModelId },
moonshot: { field: "apiModelId", default: moonshotDefaultModelId },
minimax: { field: "apiModelId", default: minimaxDefaultModelId },
mistral: { field: "apiModelId", default: mistralDefaultModelId },
xai: { field: "apiModelId", default: xaiDefaultModelId },
baseten: { field: "apiModelId", default: basetenDefaultModelId },
bedrock: { field: "apiModelId", default: bedrockDefaultModelId },
vertex: { field: "apiModelId", default: vertexDefaultModelId },
sambanova: { field: "apiModelId", default: sambaNovaDefaultModelId },
zai: {
field: "apiModelId",
default:
apiConfiguration.zaiApiLine === "china_coding"
? mainlandZAiDefaultModelId
: internationalZAiDefaultModelId,
},
fireworks: { field: "apiModelId", default: fireworksDefaultModelId },
roo: { field: "apiModelId", default: rooDefaultModelId },
"vercel-ai-gateway": { field: "vercelAiGatewayModelId", default: vercelAiGatewayDefaultModelId },
openai: { field: "openAiModelId" },
ollama: { field: "ollamaModelId" },
lmstudio: { field: "lmStudioModelId" },
}
const config = PROVIDER_MODEL_CONFIG[value]
if (config) {
validateAndResetModel(
value,
apiConfiguration[config.field] as string | undefined,
config.field,
config.default,
)
}
},
[setApiConfigurationField, apiConfiguration, organizationAllowList],
)
const modelValidationError = useMemo(() => {
return getModelValidationError(apiConfiguration, routerModels, organizationAllowList)
}, [apiConfiguration, routerModels, organizationAllowList])
const docs = useMemo(() => {
const provider = PROVIDERS.find(({ value }) => value === selectedProvider)
const name = provider?.label
if (!name) {
return undefined
}
// Get the URL slug - use custom mapping if available, otherwise use the provider key.
const slugs: Record<string, string> = {
"openai-native": "openai",
openai: "openai-compatible",
}
const slug = slugs[selectedProvider] || selectedProvider
return {
url: buildDocLink(`providers/${slug}`, "provider_docs"),
name,
}
}, [selectedProvider])
// Convert providers to SearchableSelect options
const providerOptions = useMemo(() => {
// First filter by organization allow list
const allowedProviders = filterProviders(PROVIDERS, organizationAllowList)
// Then filter out static providers that have no models (unless currently selected)
const providersWithModels = allowedProviders.filter(({ value }) => {
// Always show the currently selected provider to avoid breaking existing configurations
// Use apiConfiguration.apiProvider directly since that's what's actually selected
if (value === apiConfiguration.apiProvider) {
return true
}
// Check if this is a static provider (has models in MODELS_BY_PROVIDER)
const staticModels = MODELS_BY_PROVIDER[value as ProviderName]
// If it's a static provider, check if it has any models after filtering
if (staticModels) {
const filteredModels = filterModels(staticModels, value as ProviderName, organizationAllowList)
// Hide the provider if it has no models after filtering
return filteredModels && Object.keys(filteredModels).length > 0
}
// If it's a dynamic provider (not in MODELS_BY_PROVIDER), always show it
// to avoid race conditions with async model fetching
return true
})
const options = providersWithModels.map(({ value, label }) => ({
value,
label,
}))
// Pin "roo" to the top if not on welcome screen
if (!fromWelcomeView) {
const rooIndex = options.findIndex((opt) => opt.value === "roo")
if (rooIndex > 0) {
const [rooOption] = options.splice(rooIndex, 1)
options.unshift(rooOption)
}
} else {
// Filter out roo from the welcome view
const filteredOptions = options.filter((opt) => opt.value !== "roo")
options.length = 0
options.push(...filteredOptions)
const openRouterIndex = options.findIndex((opt) => opt.value === "openrouter")
if (openRouterIndex > 0) {
const [openRouterOption] = options.splice(openRouterIndex, 1)
options.unshift(openRouterOption)
}
}
return options
}, [organizationAllowList, apiConfiguration.apiProvider, fromWelcomeView])
return (
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-1 relative">
<div className="flex justify-between items-center">
<label className="block font-medium">{t("settings:providers.apiProvider")}</label>
{selectedProvider === "roo" && cloudIsAuthenticated ? (
<RooBalanceDisplay />
) : (
docs && (
<VSCodeLink href={docs.url} target="_blank" className="flex gap-2">
{t("settings:providers.apiProviderDocs")}
<BookOpenText className="size-4 inline ml-2" />
</VSCodeLink>
)
)}
</div>
<SearchableSelect
value={selectedProvider}
onValueChange={(value) => onProviderChange(value as ProviderName)}
options={providerOptions}
placeholder={t("settings:common.select")}
searchPlaceholder={t("settings:providers.searchProviderPlaceholder")}
emptyMessage={t("settings:providers.noProviderMatchFound")}
className="w-full"
data-testid="provider-select"
/>
</div>
{errorMessage && <ApiErrorMessage errorMessage={errorMessage} />}
{isRetiredSelectedProvider ? (
<div
className="rounded-md border border-vscode-panel-border px-3 py-2 text-sm text-vscode-descriptionForeground"
data-testid="retired-provider-message">
{t("settings:providers.retiredProviderMessage")}
</div>
) : (
<>
{selectedProvider === "openrouter" && (
<OpenRouter
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
selectedModelId={selectedModelId}
uriScheme={uriScheme}
simplifySettings={fromWelcomeView}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
/>
)}
{selectedProvider === "requesty" && (
<Requesty
uriScheme={uriScheme}
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
refetchRouterModels={refetchRouterModels}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "anthropic" && (
<Anthropic
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "openai-codex" && (
<OpenAICodex
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
openAiCodexIsAuthenticated={openAiCodexIsAuthenticated}
/>
)}
{selectedProvider === "openai-native" && (
<OpenAI
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
selectedModelInfo={selectedModelInfo}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "mistral" && (
<Mistral
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "baseten" && (
<Baseten
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "bedrock" && (
<Bedrock
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
selectedModelInfo={selectedModelInfo}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "vertex" && (
<Vertex
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "gemini" && (
<Gemini
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "openai" && (
<OpenAICompatible
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "lmstudio" && (
<LMStudio
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "deepseek" && (
<DeepSeek
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "qwen-code" && (
<QwenCode
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "moonshot" && (
<Moonshot
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "minimax" && (
<MiniMax
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "vscode-lm" && (
<VSCodeLM
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "ollama" && (
<Ollama
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "xai" && (
<XAI apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "litellm" && (
<LiteLLM
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "sambanova" && (
<SambaNova
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "zai" && (
<ZAi apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "vercel-ai-gateway" && (
<VercelAiGateway
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "fireworks" && (
<Fireworks
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "roo" && (
<Roo
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
cloudIsAuthenticated={cloudIsAuthenticated}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{/* Generic model picker for providers with static models */}
{activeSelectedProvider && shouldUseGenericModelPicker(activeSelectedProvider) && (
<>
<ModelPicker
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
defaultModelId={getDefaultModelIdForProvider(activeSelectedProvider, apiConfiguration)}
models={getStaticModelsForProvider(
activeSelectedProvider,
t("settings:labels.useCustomArn"),
)}
modelIdKey="apiModelId"
serviceName={getProviderServiceConfig(activeSelectedProvider).serviceName}
serviceUrl={getProviderServiceConfig(activeSelectedProvider).serviceUrl}
organizationAllowList={organizationAllowList}
errorMessage={modelValidationError}
simplifySettings={fromWelcomeView}
onModelChange={(modelId) =>
handleModelChangeSideEffects(
activeSelectedProvider,
modelId,
setApiConfigurationField,
)
}
/>
{selectedProvider === "bedrock" && selectedModelId === "custom-arn" && (
<BedrockCustomArn
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
</>
)}
{!fromWelcomeView && (
<ThinkingBudget
key={`${selectedProvider}-${selectedModelId}`}
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
modelInfo={selectedModelInfo}
/>
)}
{/* Gate Verbosity UI by capability flag */}
{!fromWelcomeView && selectedModelInfo?.supportsVerbosity && (
<Verbosity
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
modelInfo={selectedModelInfo}
/>
)}
{!fromWelcomeView && (
<Collapsible open={isAdvancedSettingsOpen} onOpenChange={setIsAdvancedSettingsOpen}>
<CollapsibleTrigger className="flex items-center gap-1 w-full cursor-pointer hover:opacity-80 mb-2">
<span
className={`codicon codicon-chevron-${isAdvancedSettingsOpen ? "down" : "right"}`}></span>
<span className="font-medium">{t("settings:advancedSettings.title")}</span>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-3">
<TodoListSettingsControl
todoListEnabled={apiConfiguration.todoListEnabled}
onChange={(field, value) => setApiConfigurationField(field, value)}
/>
{selectedModelInfo?.supportsTemperature !== false && (
<TemperatureControl
value={apiConfiguration.modelTemperature}
onChange={handleInputChange("modelTemperature", noTransform)}
maxValue={2}
defaultValue={selectedModelInfo?.defaultTemperature}
/>
)}
<RateLimitSecondsControl
value={apiConfiguration.rateLimitSeconds || 0}
onChange={(value) => setApiConfigurationField("rateLimitSeconds", value)}
/>
<ConsecutiveMistakeLimitControl
value={
apiConfiguration.consecutiveMistakeLimit !== undefined
? apiConfiguration.consecutiveMistakeLimit
: DEFAULT_CONSECUTIVE_MISTAKE_LIMIT
}
onChange={(value) => setApiConfigurationField("consecutiveMistakeLimit", value)}
/>
{selectedProvider === "openrouter" &&
openRouterModelProviders &&
Object.keys(openRouterModelProviders).length > 0 && (
<div>
<div className="flex items-center gap-1">
<label className="block font-medium mb-1">
{t("settings:providers.openRouter.providerRouting.title")}
</label>
<a href={`https://openrouter.ai/${selectedModelId}/providers`}>
<ExternalLinkIcon className="w-4 h-4" />
</a>
</div>
<Select
value={
apiConfiguration?.openRouterSpecificProvider ||
OPENROUTER_DEFAULT_PROVIDER_NAME
}
onValueChange={(value) =>
setApiConfigurationField("openRouterSpecificProvider", value)
}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
<SelectItem value={OPENROUTER_DEFAULT_PROVIDER_NAME}>
{OPENROUTER_DEFAULT_PROVIDER_NAME}
</SelectItem>
{Object.entries(openRouterModelProviders).map(
([value, { label }]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
),
)}
</SelectContent>
</Select>
<div className="text-sm text-vscode-descriptionForeground mt-1">
{t("settings:providers.openRouter.providerRouting.description")}{" "}
<a href="https://openrouter.ai/docs/features/provider-routing">
{t("settings:providers.openRouter.providerRouting.learnMore")}.
</a>
</div>
</div>
)}
</CollapsibleContent>
</Collapsible>
)}
</>
)}
</div>
)
}
export default memo(ApiOptions)