فهرست منبع

Move remaining provider settings into separate components (#3208)

Chris Estreich 7 ماه پیش
والد
کامیت
a649a53ec6
38فایلهای تغییر یافته به همراه917 افزوده شده و 558 حذف شده
  1. 0 2
      evals/packages/types/src/roo-code.ts
  2. 2 2
      src/core/webview/ClineProvider.ts
  3. 0 1
      src/exports/roo-code.d.ts
  4. 0 1
      src/exports/types.ts
  5. 0 2
      src/schemas/index.ts
  6. 1 1
      src/shared/api.ts
  7. 95 420
      webview-ui/src/components/settings/ApiOptions.tsx
  8. 9 9
      webview-ui/src/components/settings/constants.ts
  9. 53 0
      webview-ui/src/components/settings/providers/BedrockCustomArn.tsx
  10. 50 0
      webview-ui/src/components/settings/providers/Chutes.tsx
  11. 50 0
      webview-ui/src/components/settings/providers/DeepSeek.tsx
  12. 63 0
      webview-ui/src/components/settings/providers/Glama.tsx
  13. 50 0
      webview-ui/src/components/settings/providers/Groq.tsx
  14. 67 0
      webview-ui/src/components/settings/providers/Mistral.tsx
  15. 64 1
      webview-ui/src/components/settings/providers/OpenRouter.tsx
  16. 97 0
      webview-ui/src/components/settings/providers/Requesty.tsx
  17. 0 0
      webview-ui/src/components/settings/providers/RequestyBalanceDisplay.tsx
  18. 61 0
      webview-ui/src/components/settings/providers/Unbound.tsx
  19. 50 0
      webview-ui/src/components/settings/providers/XAI.tsx
  20. 18 0
      webview-ui/src/components/settings/providers/index.ts
  21. 122 81
      webview-ui/src/components/ui/hooks/useSelectedModel.ts
  22. 4 2
      webview-ui/src/i18n/locales/ca/settings.json
  23. 4 2
      webview-ui/src/i18n/locales/de/settings.json
  24. 4 2
      webview-ui/src/i18n/locales/en/settings.json
  25. 4 2
      webview-ui/src/i18n/locales/es/settings.json
  26. 4 2
      webview-ui/src/i18n/locales/fr/settings.json
  27. 4 2
      webview-ui/src/i18n/locales/hi/settings.json
  28. 4 2
      webview-ui/src/i18n/locales/it/settings.json
  29. 4 2
      webview-ui/src/i18n/locales/ja/settings.json
  30. 4 2
      webview-ui/src/i18n/locales/ko/settings.json
  31. 4 2
      webview-ui/src/i18n/locales/pl/settings.json
  32. 4 2
      webview-ui/src/i18n/locales/pt-BR/settings.json
  33. 4 2
      webview-ui/src/i18n/locales/ru/settings.json
  34. 4 2
      webview-ui/src/i18n/locales/tr/settings.json
  35. 4 2
      webview-ui/src/i18n/locales/vi/settings.json
  36. 4 2
      webview-ui/src/i18n/locales/zh-CN/settings.json
  37. 4 2
      webview-ui/src/i18n/locales/zh-TW/settings.json
  38. 1 6
      webview-ui/src/oauth/urls.ts

+ 0 - 2
evals/packages/types/src/roo-code.ts

@@ -355,7 +355,6 @@ export const providerSettingsSchema = z.object({
 	awsRegion: z.string().optional(),
 	awsUseCrossRegionInference: z.boolean().optional(),
 	awsUsePromptCache: z.boolean().optional(),
-	awspromptCacheId: z.string().optional(),
 	awsProfile: z.string().optional(),
 	awsUseProfile: z.boolean().optional(),
 	awsCustomArn: z.string().optional(),
@@ -455,7 +454,6 @@ const providerSettingsRecord: ProviderSettingsRecord = {
 	awsRegion: undefined,
 	awsUseCrossRegionInference: undefined,
 	awsUsePromptCache: undefined,
-	awspromptCacheId: undefined,
 	awsProfile: undefined,
 	awsUseProfile: undefined,
 	awsCustomArn: undefined,

+ 2 - 2
src/core/webview/ClineProvider.ts

@@ -13,8 +13,8 @@ import { GlobalState, ProviderSettings, RooCodeSettings } from "../../schemas"
 import { t } from "../../i18n"
 import { setPanel } from "../../activate/registerCommands"
 import {
+	ProviderName,
 	ApiConfiguration,
-	ApiProvider,
 	requestyDefaultModelId,
 	openRouterDefaultModelId,
 	glamaDefaultModelId,
@@ -1297,7 +1297,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
 		const customModes = await this.customModesManager.getCustomModes()
 
 		// Determine apiProvider with the same logic as before.
-		const apiProvider: ApiProvider = stateValues.apiProvider ? stateValues.apiProvider : "anthropic"
+		const apiProvider: ProviderName = stateValues.apiProvider ? stateValues.apiProvider : "anthropic"
 
 		// Build the apiConfiguration object combining state values and secrets.
 		const providerSettings = this.contextProxy.getProviderSettings()

+ 0 - 1
src/exports/roo-code.d.ts

@@ -42,7 +42,6 @@ type ProviderSettings = {
 	awsRegion?: string | undefined
 	awsUseCrossRegionInference?: boolean | undefined
 	awsUsePromptCache?: boolean | undefined
-	awspromptCacheId?: string | undefined
 	awsProfile?: string | undefined
 	awsUseProfile?: boolean | undefined
 	awsCustomArn?: string | undefined

+ 0 - 1
src/exports/types.ts

@@ -43,7 +43,6 @@ type ProviderSettings = {
 	awsRegion?: string | undefined
 	awsUseCrossRegionInference?: boolean | undefined
 	awsUsePromptCache?: boolean | undefined
-	awspromptCacheId?: string | undefined
 	awsProfile?: string | undefined
 	awsUseProfile?: boolean | undefined
 	awsCustomArn?: string | undefined

+ 0 - 2
src/schemas/index.ts

@@ -366,7 +366,6 @@ export const providerSettingsSchema = z.object({
 	awsRegion: z.string().optional(),
 	awsUseCrossRegionInference: z.boolean().optional(),
 	awsUsePromptCache: z.boolean().optional(),
-	awspromptCacheId: z.string().optional(),
 	awsProfile: z.string().optional(),
 	awsUseProfile: z.boolean().optional(),
 	awsCustomArn: z.string().optional(),
@@ -471,7 +470,6 @@ const providerSettingsRecord: ProviderSettingsRecord = {
 	awsRegion: undefined,
 	awsUseCrossRegionInference: undefined,
 	awsUsePromptCache: undefined,
-	awspromptCacheId: undefined,
 	awsProfile: undefined,
 	awsUseProfile: undefined,
 	awsCustomArn: undefined,

+ 1 - 1
src/shared/api.ts

@@ -1,6 +1,6 @@
 import { ModelInfo, ProviderName, ProviderSettings } from "../schemas"
 
-export type { ModelInfo, ProviderName as ApiProvider }
+export type { ModelInfo, ProviderName }
 
 export type ApiHandlerOptions = Omit<ProviderSettings, "apiProvider" | "id">
 

+ 95 - 420
webview-ui/src/components/settings/ApiOptions.tsx

@@ -1,55 +1,55 @@
 import React, { memo, useCallback, useEffect, useMemo, useState } from "react"
 import { useDebounce } from "react-use"
-import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
-import { ExternalLinkIcon } from "@radix-ui/react-icons"
+import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
 
 import {
-	ApiConfiguration,
-	glamaDefaultModelId,
-	mistralDefaultModelId,
+	type ProviderName,
+	type ApiConfiguration,
 	openRouterDefaultModelId,
-	unboundDefaultModelId,
 	requestyDefaultModelId,
-	ApiProvider,
+	glamaDefaultModelId,
+	unboundDefaultModelId,
 } from "@roo/shared/api"
 
 import { vscode } from "@src/utils/vscode"
-import { validateApiConfiguration, validateModelId, validateBedrockArn } from "@src/utils/validate"
+import { validateApiConfiguration, validateModelId } 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
+
 import {
-	useOpenRouterModelProviders,
-	OPENROUTER_DEFAULT_PROVIDER_NAME,
-} from "@src/components/ui/hooks/useOpenRouterModelProviders"
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Button } from "@src/components/ui"
-import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
-import { getRequestyApiKeyUrl, getGlamaAuthUrl } from "@src/oauth/urls"
-
-// Providers
-import { Anthropic } from "./providers/Anthropic"
-import { Bedrock } from "./providers/Bedrock"
-import { Gemini } from "./providers/Gemini"
-import { LMStudio } from "./providers/LMStudio"
-import { Ollama } from "./providers/Ollama"
-import { OpenAI } from "./providers/OpenAI"
-import { OpenAICompatible } from "./providers/OpenAICompatible"
-import { OpenRouter } from "./providers/OpenRouter"
-import { Vertex } from "./providers/Vertex"
-import { VSCodeLM } from "./providers/VSCodeLM"
+	Anthropic,
+	Bedrock,
+	Chutes,
+	DeepSeek,
+	Gemini,
+	Glama,
+	Groq,
+	LMStudio,
+	Mistral,
+	Ollama,
+	OpenAI,
+	OpenAICompatible,
+	OpenRouter,
+	Requesty,
+	Unbound,
+	Vertex,
+	VSCodeLM,
+	XAI,
+} from "./providers"
 
 import { MODELS_BY_PROVIDER, PROVIDERS, REASONING_MODELS } from "./constants"
 import { inputEventTransform, noTransform } from "./transforms"
 import { ModelInfoView } from "./ModelInfoView"
-import { ModelPicker } from "./ModelPicker"
 import { ApiErrorMessage } from "./ApiErrorMessage"
 import { ThinkingBudget } from "./ThinkingBudget"
-import { RequestyBalanceDisplay } from "./RequestyBalanceDisplay"
 import { ReasoningEffort } from "./ReasoningEffort"
 import { PromptCachingControl } from "./PromptCachingControl"
 import { DiffSettingsControl } from "./DiffSettingsControl"
 import { TemperatureControl } from "./TemperatureControl"
 import { RateLimitSecondsControl } from "./RateLimitSecondsControl"
+import { BedrockCustomArn } from "./providers/BedrockCustomArn"
 
 export interface ApiOptionsProps {
 	uriScheme: string | undefined
@@ -75,8 +75,6 @@ const ApiOptions = ({
 		return Object.entries(headers)
 	})
 
-	const [requestyShowRefreshHint, setRequestyShowRefreshHint] = useState<boolean>()
-
 	useEffect(() => {
 		const propHeaders = apiConfiguration?.openAiHeaders || {}
 
@@ -106,13 +104,14 @@ const ApiOptions = ({
 		return result
 	}
 
-	// Debounced effect to update the main configuration when local customHeaders state stabilizes.
+	// 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
+			// Only update if the processed object is different from the current config.
 			if (JSON.stringify(currentConfigHeaders) !== JSON.stringify(newHeadersObject)) {
 				setApiConfigurationField("openAiHeaders", newHeadersObject)
 			}
@@ -142,7 +141,7 @@ const ApiOptions = ({
 
 	const { data: routerModels, refetch: refetchRouterModels } = useRouterModels()
 
-	// Update apiConfiguration.aiModelId whenever selectedModelId changes.
+	// Update `apiModelId` whenever `selectedModelId` changes.
 	useEffect(() => {
 		if (selectedModelId) {
 			setApiConfigurationField("apiModelId", selectedModelId)
@@ -193,16 +192,7 @@ const ApiOptions = ({
 		setErrorMessage(apiValidationResult)
 	}, [apiConfiguration, routerModels, setErrorMessage])
 
-	const { data: openRouterModelProviders } = useOpenRouterModelProviders(apiConfiguration?.openRouterModelId, {
-		enabled:
-			selectedProvider === "openrouter" &&
-			!!apiConfiguration?.openRouterModelId &&
-			routerModels?.openrouter &&
-			Object.keys(routerModels.openrouter).length > 1 &&
-			apiConfiguration.openRouterModelId in routerModels.openrouter,
-	})
-
-	const selectedProviderModelOptions = useMemo(
+	const selectedProviderModels = useMemo(
 		() =>
 			MODELS_BY_PROVIDER[selectedProvider]
 				? Object.keys(MODELS_BY_PROVIDER[selectedProvider]).map((modelId) => ({
@@ -213,37 +203,8 @@ const ApiOptions = ({
 		[selectedProvider],
 	)
 
-	// Custom URL path mappings for providers with different slugs.
-	const providerUrlSlugs: Record<string, string> = {
-		"openai-native": "openai",
-		openai: "openai-compatible",
-	}
-
-	// Helper function to get provider display name from PROVIDERS constant.
-	const getProviderDisplayName = (providerKey: string): string | undefined => {
-		const provider = PROVIDERS.find((p) => p.value === providerKey)
-		return provider?.label
-	}
-
-	// Helper function to get the documentation URL and name for the currently selected provider
-	const getSelectedProviderDocUrl = (): { url: string; name: string } | undefined => {
-		const displayName = getProviderDisplayName(selectedProvider)
-
-		if (!displayName) {
-			return undefined
-		}
-
-		// Get the URL slug - use custom mapping if available, otherwise use the provider key
-		const urlSlug = providerUrlSlugs[selectedProvider] || selectedProvider
-
-		return {
-			url: `https://docs.roocode.com/providers/${urlSlug}`,
-			name: displayName,
-		}
-	}
-
-	const onApiProviderChange = useCallback(
-		(value: ApiProvider) => {
+	const onProviderChange = useCallback(
+		(value: ProviderName) => {
 			// It would be much easier to have a single attribute that stores
 			// the modelId, but we have a separate attribute for each of
 			// OpenRouter, Glama, Unbound, and Requesty.
@@ -285,25 +246,40 @@ const ApiOptions = ({
 		],
 	)
 
+	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",
+		}
+
+		return {
+			url: `https://docs.roocode.com/providers/${slugs[selectedProvider] || selectedProvider}`,
+			name,
+		}
+	}, [selectedProvider])
+
 	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 mb-1">{t("settings:providers.apiProvider")}</label>
-					{getSelectedProviderDocUrl() && (
+					{docs && (
 						<div className="text-xs text-vscode-descriptionForeground">
-							<VSCodeLink
-								href={getSelectedProviderDocUrl()!.url}
-								className="hover:text-vscode-foreground"
-								target="_blank">
-								{t("settings:providers.providerDocumentation", {
-									provider: getSelectedProviderDocUrl()!.name,
-								})}
+							<VSCodeLink href={docs.url} className="hover:text-vscode-foreground" target="_blank">
+								{t("settings:providers.providerDocumentation", { provider: docs.name })}
 							</VSCodeLink>
 						</div>
 					)}
 				</div>
-				<Select value={selectedProvider} onValueChange={(value) => onApiProviderChange(value as ApiProvider)}>
+				<Select value={selectedProvider} onValueChange={(value) => onProviderChange(value as ProviderName)}>
 					<SelectTrigger className="w-full">
 						<SelectValue placeholder={t("settings:common.select")} />
 					</SelectTrigger>
@@ -323,81 +299,41 @@ const ApiOptions = ({
 				<OpenRouter
 					apiConfiguration={apiConfiguration}
 					setApiConfigurationField={setApiConfigurationField}
+					routerModels={routerModels}
+					selectedModelId={selectedModelId}
 					uriScheme={uriScheme}
 					fromWelcomeView={fromWelcomeView}
 				/>
 			)}
 
-			{selectedProvider === "anthropic" && (
-				<Anthropic apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
+			{selectedProvider === "requesty" && (
+				<Requesty
+					apiConfiguration={apiConfiguration}
+					setApiConfigurationField={setApiConfigurationField}
+					routerModels={routerModels}
+					refetchRouterModels={refetchRouterModels}
+				/>
 			)}
 
 			{selectedProvider === "glama" && (
-				<>
-					<VSCodeTextField
-						value={apiConfiguration?.glamaApiKey || ""}
-						type="password"
-						onInput={handleInputChange("glamaApiKey")}
-						placeholder={t("settings:placeholders.apiKey")}
-						className="w-full">
-						<label className="block font-medium mb-1">{t("settings:providers.glamaApiKey")}</label>
-					</VSCodeTextField>
-					<div className="text-sm text-vscode-descriptionForeground -mt-2">
-						{t("settings:providers.apiKeyStorageNotice")}
-					</div>
-					{!apiConfiguration?.glamaApiKey && (
-						<VSCodeButtonLink
-							href={getGlamaAuthUrl(uriScheme)}
-							style={{ width: "100%" }}
-							appearance="primary">
-							{t("settings:providers.getGlamaApiKey")}
-						</VSCodeButtonLink>
-					)}
-				</>
+				<Glama
+					apiConfiguration={apiConfiguration}
+					setApiConfigurationField={setApiConfigurationField}
+					routerModels={routerModels}
+					uriScheme={uriScheme}
+				/>
 			)}
 
-			{selectedProvider === "requesty" && (
-				<>
-					<VSCodeTextField
-						value={apiConfiguration?.requestyApiKey || ""}
-						type="password"
-						onInput={handleInputChange("requestyApiKey")}
-						placeholder={t("settings:providers.getRequestyApiKey")}
-						className="w-full">
-						<div className="flex justify-between items-center mb-1">
-							<label className="block font-medium">{t("settings:providers.requestyApiKey")}</label>
-							{apiConfiguration?.requestyApiKey && (
-								<RequestyBalanceDisplay apiKey={apiConfiguration.requestyApiKey} />
-							)}
-						</div>
-					</VSCodeTextField>
-					<div className="text-sm text-vscode-descriptionForeground -mt-2">
-						{t("settings:providers.apiKeyStorageNotice")}
-					</div>
-					{!apiConfiguration?.requestyApiKey && (
-						<VSCodeButtonLink href={getRequestyApiKeyUrl()} style={{ width: "100%" }} appearance="primary">
-							{t("settings:providers.getRequestyApiKey")}
-						</VSCodeButtonLink>
-					)}
-					<Button
-						variant="outline"
-						title={t("settings:providers.refetchModels")}
-						onClick={() => {
-							vscode.postMessage({ type: "flushRouterModels", text: "requesty" })
-							refetchRouterModels()
-							setRequestyShowRefreshHint(true)
-						}}>
-						<div className="flex items-center gap-2">
-							<span className="codicon codicon-refresh" />
-							{t("settings:providers.flushModelsCache")}
-						</div>
-					</Button>
-					{requestyShowRefreshHint && (
-						<div className="flex items-center text-vscode-errorForeground">
-							{t("settings:providers.flushedModelsCache")}
-						</div>
-					)}
-				</>
+			{selectedProvider === "unbound" && (
+				<Unbound
+					apiConfiguration={apiConfiguration}
+					setApiConfigurationField={setApiConfigurationField}
+					routerModels={routerModels}
+				/>
+			)}
+
+			{selectedProvider === "anthropic" && (
+				<Anthropic apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
 			)}
 
 			{selectedProvider === "openai-native" && (
@@ -405,42 +341,7 @@ const ApiOptions = ({
 			)}
 
 			{selectedProvider === "mistral" && (
-				<>
-					<VSCodeTextField
-						value={apiConfiguration?.mistralApiKey || ""}
-						type="password"
-						onInput={handleInputChange("mistralApiKey")}
-						placeholder={t("settings:placeholders.apiKey")}
-						className="w-full">
-						<span className="font-medium">{t("settings:providers.mistralApiKey")}</span>
-					</VSCodeTextField>
-					<div className="text-sm text-vscode-descriptionForeground -mt-2">
-						{t("settings:providers.apiKeyStorageNotice")}
-					</div>
-					{!apiConfiguration?.mistralApiKey && (
-						<VSCodeButtonLink href="https://console.mistral.ai/" appearance="secondary">
-							{t("settings:providers.getMistralApiKey")}
-						</VSCodeButtonLink>
-					)}
-					{(apiConfiguration?.apiModelId?.startsWith("codestral-") ||
-						(!apiConfiguration?.apiModelId && mistralDefaultModelId.startsWith("codestral-"))) && (
-						<>
-							<VSCodeTextField
-								value={apiConfiguration?.mistralCodestralUrl || ""}
-								type="url"
-								onInput={handleInputChange("mistralCodestralUrl")}
-								placeholder="https://codestral.mistral.ai"
-								className="w-full">
-								<label className="block font-medium mb-1">
-									{t("settings:providers.codestralBaseUrl")}
-								</label>
-							</VSCodeTextField>
-							<div className="text-sm text-vscode-descriptionForeground -mt-2">
-								{t("settings:providers.codestralBaseUrlDesc")}
-							</div>
-						</>
-					)}
-				</>
+				<Mistral apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
 			)}
 
 			{selectedProvider === "bedrock" && (
@@ -471,24 +372,7 @@ const ApiOptions = ({
 			)}
 
 			{selectedProvider === "deepseek" && (
-				<>
-					<VSCodeTextField
-						value={apiConfiguration?.deepSeekApiKey || ""}
-						type="password"
-						onInput={handleInputChange("deepSeekApiKey")}
-						placeholder={t("settings:placeholders.apiKey")}
-						className="w-full">
-						<label className="block font-medium mb-1">{t("settings:providers.deepSeekApiKey")}</label>
-					</VSCodeTextField>
-					<div className="text-sm text-vscode-descriptionForeground -mt-2">
-						{t("settings:providers.apiKeyStorageNotice")}
-					</div>
-					{!apiConfiguration?.deepSeekApiKey && (
-						<VSCodeButtonLink href="https://platform.deepseek.com/" appearance="secondary">
-							{t("settings:providers.getDeepSeekApiKey")}
-						</VSCodeButtonLink>
-					)}
-				</>
+				<DeepSeek apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
 			)}
 
 			{selectedProvider === "vscode-lm" && (
@@ -500,87 +384,15 @@ const ApiOptions = ({
 			)}
 
 			{selectedProvider === "xai" && (
-				<>
-					<VSCodeTextField
-						value={apiConfiguration?.xaiApiKey || ""}
-						type="password"
-						onInput={handleInputChange("xaiApiKey")}
-						placeholder={t("settings:placeholders.apiKey")}
-						className="w-full">
-						<label className="block font-medium mb-1">{t("settings:providers.xaiApiKey")}</label>
-					</VSCodeTextField>
-					<div className="text-sm text-vscode-descriptionForeground -mt-2">
-						{t("settings:providers.apiKeyStorageNotice")}
-					</div>
-					{!apiConfiguration?.xaiApiKey && (
-						<VSCodeButtonLink href="https://api.x.ai/docs" appearance="secondary">
-							{t("settings:providers.getXaiApiKey")}
-						</VSCodeButtonLink>
-					)}
-				</>
+				<XAI apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
 			)}
 
 			{selectedProvider === "groq" && (
-				<>
-					<VSCodeTextField
-						value={apiConfiguration?.groqApiKey || ""}
-						type="password"
-						onInput={handleInputChange("groqApiKey")}
-						placeholder={t("settings:placeholders.apiKey")}
-						className="w-full">
-						<label className="block font-medium mb-1">{t("settings:providers.groqApiKey")}</label>
-					</VSCodeTextField>
-					<div className="text-sm text-vscode-descriptionForeground -mt-2">
-						{t("settings:providers.apiKeyStorageNotice")}
-					</div>
-					{!apiConfiguration?.groqApiKey && (
-						<VSCodeButtonLink href="https://console.groq.com/keys" appearance="secondary">
-							{t("settings:providers.getGroqApiKey")}
-						</VSCodeButtonLink>
-					)}
-				</>
+				<Groq apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
 			)}
 
 			{selectedProvider === "chutes" && (
-				<>
-					<VSCodeTextField
-						value={apiConfiguration?.chutesApiKey || ""}
-						type="password"
-						onInput={handleInputChange("chutesApiKey")}
-						placeholder={t("settings:placeholders.apiKey")}
-						className="w-full">
-						<label className="block font-medium mb-1">{t("settings:providers.chutesApiKey")}</label>
-					</VSCodeTextField>
-					<div className="text-sm text-vscode-descriptionForeground -mt-2">
-						{t("settings:providers.apiKeyStorageNotice")}
-					</div>
-					{!apiConfiguration?.chutesApiKey && (
-						<VSCodeButtonLink href="https://chutes.ai/app/api" appearance="secondary">
-							{t("settings:providers.getChutesApiKey")}
-						</VSCodeButtonLink>
-					)}
-				</>
-			)}
-
-			{selectedProvider === "unbound" && (
-				<>
-					<VSCodeTextField
-						value={apiConfiguration?.unboundApiKey || ""}
-						type="password"
-						onInput={handleInputChange("unboundApiKey")}
-						placeholder={t("settings:placeholders.apiKey")}
-						className="w-full">
-						<label className="block font-medium mb-1">{t("settings:providers.unboundApiKey")}</label>
-					</VSCodeTextField>
-					<div className="text-sm text-vscode-descriptionForeground -mt-2">
-						{t("settings:providers.apiKeyStorageNotice")}
-					</div>
-					{!apiConfiguration?.unboundApiKey && (
-						<VSCodeButtonLink href="https://gateway.getunbound.ai" appearance="secondary">
-							{t("settings:providers.getUnboundApiKey")}
-						</VSCodeButtonLink>
-					)}
-				</>
+				<Chutes apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
 			)}
 
 			{selectedProvider === "human-relay" && (
@@ -594,99 +406,10 @@ const ApiOptions = ({
 				</>
 			)}
 
-			{/* Model Pickers */}
-
-			{selectedProvider === "openrouter" && (
-				<ModelPicker
-					apiConfiguration={apiConfiguration}
-					setApiConfigurationField={setApiConfigurationField}
-					defaultModelId={openRouterDefaultModelId}
-					models={routerModels?.openrouter ?? {}}
-					modelIdKey="openRouterModelId"
-					serviceName="OpenRouter"
-					serviceUrl="https://openrouter.ai/models"
-				/>
-			)}
-
-			{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>
-				)}
-
-			{selectedProvider === "glama" && (
-				<ModelPicker
-					apiConfiguration={apiConfiguration}
-					setApiConfigurationField={setApiConfigurationField}
-					defaultModelId={glamaDefaultModelId}
-					models={routerModels?.glama ?? {}}
-					modelIdKey="glamaModelId"
-					serviceName="Glama"
-					serviceUrl="https://glama.ai/models"
-				/>
-			)}
-
-			{selectedProvider === "unbound" && (
-				<ModelPicker
-					apiConfiguration={apiConfiguration}
-					defaultModelId={unboundDefaultModelId}
-					models={routerModels?.unbound ?? {}}
-					modelIdKey="unboundModelId"
-					serviceName="Unbound"
-					serviceUrl="https://api.getunbound.ai/models"
-					setApiConfigurationField={setApiConfigurationField}
-				/>
-			)}
-
-			{selectedProvider === "requesty" && (
-				<ModelPicker
-					apiConfiguration={apiConfiguration}
-					setApiConfigurationField={setApiConfigurationField}
-					defaultModelId={requestyDefaultModelId}
-					models={routerModels?.requesty ?? {}}
-					modelIdKey="requestyModelId"
-					serviceName="Requesty"
-					serviceUrl="https://requesty.ai"
-				/>
-			)}
-
-			{selectedProviderModelOptions.length > 0 && (
+			{selectedProviderModels.length > 0 && (
 				<>
 					<div>
 						<label className="block font-medium mb-1">{t("settings:providers.model")}</label>
-
 						<Select
 							value={selectedModelId === "custom-arn" ? "custom-arn" : selectedModelId}
 							onValueChange={(value) => {
@@ -701,7 +424,7 @@ const ApiOptions = ({
 								<SelectValue placeholder={t("settings:common.select")} />
 							</SelectTrigger>
 							<SelectContent>
-								{selectedProviderModelOptions.map((option) => (
+								{selectedProviderModels.map((option) => (
 									<SelectItem key={option.value} value={option.value}>
 										{option.label}
 									</SelectItem>
@@ -714,58 +437,10 @@ const ApiOptions = ({
 					</div>
 
 					{selectedProvider === "bedrock" && selectedModelId === "custom-arn" && (
-						<>
-							<VSCodeTextField
-								value={apiConfiguration?.awsCustomArn || ""}
-								onInput={(e) => {
-									const value = (e.target as HTMLInputElement).value
-									setApiConfigurationField("awsCustomArn", value)
-								}}
-								placeholder={t("settings:placeholders.customArn")}
-								className="w-full">
-								<label className="block font-medium mb-1">{t("settings:labels.customArn")}</label>
-							</VSCodeTextField>
-							<div className="text-sm text-vscode-descriptionForeground -mt-2">
-								{t("settings:providers.awsCustomArnUse")}
-								<ul className="list-disc pl-5 mt-1">
-									<li>
-										arn:aws:bedrock:eu-west-1:123456789012:inference-profile/eu.anthropic.claude-3-7-sonnet-20250219-v1:0
-									</li>
-									<li>
-										arn:aws:bedrock:us-west-2:123456789012:provisioned-model/my-provisioned-model
-									</li>
-									<li>
-										arn:aws:bedrock:us-east-1:123456789012:default-prompt-router/anthropic.claude:1
-									</li>
-								</ul>
-								{t("settings:providers.awsCustomArnDesc")}
-							</div>
-							{apiConfiguration?.awsCustomArn &&
-								(() => {
-									const validation = validateBedrockArn(
-										apiConfiguration.awsCustomArn,
-										apiConfiguration.awsRegion,
-									)
-
-									if (!validation.isValid) {
-										return (
-											<div className="text-sm text-vscode-errorForeground mt-2">
-												{validation.errorMessage || t("settings:providers.invalidArnFormat")}
-											</div>
-										)
-									}
-
-									if (validation.errorMessage) {
-										return (
-											<div className="text-sm text-vscode-errorForeground mt-2">
-												{validation.errorMessage}
-											</div>
-										)
-									}
-
-									return null
-								})()}
-						</>
+						<BedrockCustomArn
+							apiConfiguration={apiConfiguration}
+							setApiConfigurationField={setApiConfigurationField}
+						/>
 					)}
 
 					<ModelInfoView

+ 9 - 9
webview-ui/src/components/settings/constants.ts

@@ -1,5 +1,5 @@
 import {
-	ApiProvider,
+	ProviderName,
 	ModelInfo,
 	anthropicModels,
 	bedrockModels,
@@ -9,15 +9,15 @@ import {
 	openAiNativeModels,
 	vertexModels,
 	xaiModels,
-	groqModels, 
-	chutesModels, 
+	groqModels,
+	chutesModels,
 } from "@roo/shared/api"
 
 export { REASONING_MODELS, PROMPT_CACHING_MODELS } from "@roo/shared/api"
 
 export { AWS_REGIONS } from "@roo/shared/aws_regions"
 
-export const MODELS_BY_PROVIDER: Partial<Record<ApiProvider, Record<string, ModelInfo>>> = {
+export const MODELS_BY_PROVIDER: Partial<Record<ProviderName, Record<string, ModelInfo>>> = {
 	anthropic: anthropicModels,
 	bedrock: bedrockModels,
 	deepseek: deepSeekModels,
@@ -26,8 +26,8 @@ export const MODELS_BY_PROVIDER: Partial<Record<ApiProvider, Record<string, Mode
 	"openai-native": openAiNativeModels,
 	vertex: vertexModels,
 	xai: xaiModels,
-	groq: groqModels, 
-	chutes: chutesModels, 
+	groq: groqModels,
+	chutes: chutesModels,
 }
 
 export const PROVIDERS = [
@@ -47,9 +47,9 @@ export const PROVIDERS = [
 	{ value: "unbound", label: "Unbound" },
 	{ value: "requesty", label: "Requesty" },
 	{ value: "human-relay", label: "Human Relay" },
-	{ value: "xai", label: "xAI" },
-	{ value: "groq", label: "Groq" }, 
-	{ value: "chutes", label: "Chutes AI" }, 
+	{ value: "xai", label: "xAI (Grok)" },
+	{ value: "groq", label: "Groq" },
+	{ value: "chutes", label: "Chutes AI" },
 ].sort((a, b) => a.label.localeCompare(b.label))
 
 export const VERTEX_REGIONS = [

+ 53 - 0
webview-ui/src/components/settings/providers/BedrockCustomArn.tsx

@@ -0,0 +1,53 @@
+import { useMemo } from "react"
+import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
+
+import { ApiConfiguration } from "@roo/shared/api"
+
+import { validateBedrockArn } from "@src/utils/validate"
+import { useAppTranslation } from "@src/i18n/TranslationContext"
+
+type BedrockCustomArnProps = {
+	apiConfiguration: ApiConfiguration
+	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+}
+
+export const BedrockCustomArn = ({ apiConfiguration, setApiConfigurationField }: BedrockCustomArnProps) => {
+	const { t } = useAppTranslation()
+
+	const validation = useMemo(() => {
+		const { awsCustomArn, awsRegion } = apiConfiguration
+		return awsCustomArn ? validateBedrockArn(awsCustomArn, awsRegion) : { isValid: true, errorMessage: undefined }
+	}, [apiConfiguration])
+
+	return (
+		<>
+			<VSCodeTextField
+				value={apiConfiguration?.awsCustomArn || ""}
+				onInput={(e) => setApiConfigurationField("awsCustomArn", (e.target as HTMLInputElement).value)}
+				placeholder={t("settings:placeholders.customArn")}
+				className="w-full">
+				<label className="block font-medium mb-1">{t("settings:labels.customArn")}</label>
+			</VSCodeTextField>
+			<div className="text-sm text-vscode-descriptionForeground -mt-2">
+				{t("settings:providers.awsCustomArnUse")}
+				<ul className="list-disc pl-5 mt-1">
+					<li>
+						arn:aws:bedrock:eu-west-1:123456789012:inference-profile/eu.anthropic.claude-3-7-sonnet-20250219-v1:0
+					</li>
+					<li>arn:aws:bedrock:us-west-2:123456789012:provisioned-model/my-provisioned-model</li>
+					<li>arn:aws:bedrock:us-east-1:123456789012:default-prompt-router/anthropic.claude:1</li>
+				</ul>
+				{t("settings:providers.awsCustomArnDesc")}
+			</div>
+			{!validation.isValid ? (
+				<div className="text-sm text-vscode-errorForeground mt-2">
+					{validation.errorMessage || t("settings:providers.invalidArnFormat")}
+				</div>
+			) : (
+				validation.errorMessage && (
+					<div className="text-sm text-vscode-errorForeground mt-2">{validation.errorMessage}</div>
+				)
+			)}
+		</>
+	)
+}

+ 50 - 0
webview-ui/src/components/settings/providers/Chutes.tsx

@@ -0,0 +1,50 @@
+import { useCallback } from "react"
+import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
+
+import { ApiConfiguration } from "@roo/shared/api"
+
+import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
+
+import { inputEventTransform } from "../transforms"
+
+type ChutesProps = {
+	apiConfiguration: ApiConfiguration
+	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+}
+
+export const Chutes = ({ apiConfiguration, setApiConfigurationField }: ChutesProps) => {
+	const { t } = useAppTranslation()
+
+	const handleInputChange = useCallback(
+		<K extends keyof ApiConfiguration, E>(
+			field: K,
+			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+		) =>
+			(event: E | Event) => {
+				setApiConfigurationField(field, transform(event as E))
+			},
+		[setApiConfigurationField],
+	)
+
+	return (
+		<>
+			<VSCodeTextField
+				value={apiConfiguration?.chutesApiKey || ""}
+				type="password"
+				onInput={handleInputChange("chutesApiKey")}
+				placeholder={t("settings:placeholders.apiKey")}
+				className="w-full">
+				<label className="block font-medium mb-1">{t("settings:providers.chutesApiKey")}</label>
+			</VSCodeTextField>
+			<div className="text-sm text-vscode-descriptionForeground -mt-2">
+				{t("settings:providers.apiKeyStorageNotice")}
+			</div>
+			{!apiConfiguration?.chutesApiKey && (
+				<VSCodeButtonLink href="https://chutes.ai/app/api" appearance="secondary">
+					{t("settings:providers.getChutesApiKey")}
+				</VSCodeButtonLink>
+			)}
+		</>
+	)
+}

+ 50 - 0
webview-ui/src/components/settings/providers/DeepSeek.tsx

@@ -0,0 +1,50 @@
+import { useCallback } from "react"
+import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
+
+import { ApiConfiguration } from "@roo/shared/api"
+
+import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
+
+import { inputEventTransform } from "../transforms"
+
+type DeepSeekProps = {
+	apiConfiguration: ApiConfiguration
+	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+}
+
+export const DeepSeek = ({ apiConfiguration, setApiConfigurationField }: DeepSeekProps) => {
+	const { t } = useAppTranslation()
+
+	const handleInputChange = useCallback(
+		<K extends keyof ApiConfiguration, E>(
+			field: K,
+			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+		) =>
+			(event: E | Event) => {
+				setApiConfigurationField(field, transform(event as E))
+			},
+		[setApiConfigurationField],
+	)
+
+	return (
+		<>
+			<VSCodeTextField
+				value={apiConfiguration?.deepSeekApiKey || ""}
+				type="password"
+				onInput={handleInputChange("deepSeekApiKey")}
+				placeholder={t("settings:placeholders.apiKey")}
+				className="w-full">
+				<label className="block font-medium mb-1">{t("settings:providers.deepSeekApiKey")}</label>
+			</VSCodeTextField>
+			<div className="text-sm text-vscode-descriptionForeground -mt-2">
+				{t("settings:providers.apiKeyStorageNotice")}
+			</div>
+			{!apiConfiguration?.deepSeekApiKey && (
+				<VSCodeButtonLink href="https://platform.deepseek.com/" appearance="secondary">
+					{t("settings:providers.getDeepSeekApiKey")}
+				</VSCodeButtonLink>
+			)}
+		</>
+	)
+}

+ 63 - 0
webview-ui/src/components/settings/providers/Glama.tsx

@@ -0,0 +1,63 @@
+import { useCallback } from "react"
+import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
+
+import { ApiConfiguration, RouterModels, glamaDefaultModelId } from "@roo/shared/api"
+
+import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { getGlamaAuthUrl } from "@src/oauth/urls"
+import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
+
+import { inputEventTransform } from "../transforms"
+import { ModelPicker } from "../ModelPicker"
+
+type GlamaProps = {
+	apiConfiguration: ApiConfiguration
+	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	routerModels?: RouterModels
+	uriScheme?: string
+}
+
+export const Glama = ({ apiConfiguration, setApiConfigurationField, routerModels, uriScheme }: GlamaProps) => {
+	const { t } = useAppTranslation()
+
+	const handleInputChange = useCallback(
+		<K extends keyof ApiConfiguration, E>(
+			field: K,
+			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+		) =>
+			(event: E | Event) => {
+				setApiConfigurationField(field, transform(event as E))
+			},
+		[setApiConfigurationField],
+	)
+
+	return (
+		<>
+			<VSCodeTextField
+				value={apiConfiguration?.glamaApiKey || ""}
+				type="password"
+				onInput={handleInputChange("glamaApiKey")}
+				placeholder={t("settings:placeholders.apiKey")}
+				className="w-full">
+				<label className="block font-medium mb-1">{t("settings:providers.glamaApiKey")}</label>
+			</VSCodeTextField>
+			<div className="text-sm text-vscode-descriptionForeground -mt-2">
+				{t("settings:providers.apiKeyStorageNotice")}
+			</div>
+			{!apiConfiguration?.glamaApiKey && (
+				<VSCodeButtonLink href={getGlamaAuthUrl(uriScheme)} style={{ width: "100%" }} appearance="primary">
+					{t("settings:providers.getGlamaApiKey")}
+				</VSCodeButtonLink>
+			)}
+			<ModelPicker
+				apiConfiguration={apiConfiguration}
+				setApiConfigurationField={setApiConfigurationField}
+				defaultModelId={glamaDefaultModelId}
+				models={routerModels?.glama ?? {}}
+				modelIdKey="glamaModelId"
+				serviceName="Glama"
+				serviceUrl="https://glama.ai/models"
+			/>
+		</>
+	)
+}

+ 50 - 0
webview-ui/src/components/settings/providers/Groq.tsx

@@ -0,0 +1,50 @@
+import { useCallback } from "react"
+import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
+
+import { ApiConfiguration } from "@roo/shared/api"
+
+import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
+
+import { inputEventTransform } from "../transforms"
+
+type GroqProps = {
+	apiConfiguration: ApiConfiguration
+	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+}
+
+export const Groq = ({ apiConfiguration, setApiConfigurationField }: GroqProps) => {
+	const { t } = useAppTranslation()
+
+	const handleInputChange = useCallback(
+		<K extends keyof ApiConfiguration, E>(
+			field: K,
+			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+		) =>
+			(event: E | Event) => {
+				setApiConfigurationField(field, transform(event as E))
+			},
+		[setApiConfigurationField],
+	)
+
+	return (
+		<>
+			<VSCodeTextField
+				value={apiConfiguration?.groqApiKey || ""}
+				type="password"
+				onInput={handleInputChange("groqApiKey")}
+				placeholder={t("settings:placeholders.apiKey")}
+				className="w-full">
+				<label className="block font-medium mb-1">{t("settings:providers.groqApiKey")}</label>
+			</VSCodeTextField>
+			<div className="text-sm text-vscode-descriptionForeground -mt-2">
+				{t("settings:providers.apiKeyStorageNotice")}
+			</div>
+			{!apiConfiguration?.groqApiKey && (
+				<VSCodeButtonLink href="https://console.groq.com/keys" appearance="secondary">
+					{t("settings:providers.getGroqApiKey")}
+				</VSCodeButtonLink>
+			)}
+		</>
+	)
+}

+ 67 - 0
webview-ui/src/components/settings/providers/Mistral.tsx

@@ -0,0 +1,67 @@
+import { useCallback } from "react"
+import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
+
+import { ApiConfiguration, RouterModels, mistralDefaultModelId } from "@roo/shared/api"
+
+import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
+
+import { inputEventTransform } from "../transforms"
+
+type MistralProps = {
+	apiConfiguration: ApiConfiguration
+	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	routerModels?: RouterModels
+}
+
+export const Mistral = ({ apiConfiguration, setApiConfigurationField }: MistralProps) => {
+	const { t } = useAppTranslation()
+
+	const handleInputChange = useCallback(
+		<K extends keyof ApiConfiguration, E>(
+			field: K,
+			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+		) =>
+			(event: E | Event) => {
+				setApiConfigurationField(field, transform(event as E))
+			},
+		[setApiConfigurationField],
+	)
+
+	return (
+		<>
+			<VSCodeTextField
+				value={apiConfiguration?.mistralApiKey || ""}
+				type="password"
+				onInput={handleInputChange("mistralApiKey")}
+				placeholder={t("settings:placeholders.apiKey")}
+				className="w-full">
+				<span className="font-medium">{t("settings:providers.mistralApiKey")}</span>
+			</VSCodeTextField>
+			<div className="text-sm text-vscode-descriptionForeground -mt-2">
+				{t("settings:providers.apiKeyStorageNotice")}
+			</div>
+			{!apiConfiguration?.mistralApiKey && (
+				<VSCodeButtonLink href="https://console.mistral.ai/" appearance="secondary">
+					{t("settings:providers.getMistralApiKey")}
+				</VSCodeButtonLink>
+			)}
+			{(apiConfiguration?.apiModelId?.startsWith("codestral-") ||
+				(!apiConfiguration?.apiModelId && mistralDefaultModelId.startsWith("codestral-"))) && (
+				<>
+					<VSCodeTextField
+						value={apiConfiguration?.mistralCodestralUrl || ""}
+						type="url"
+						onInput={handleInputChange("mistralCodestralUrl")}
+						placeholder="https://codestral.mistral.ai"
+						className="w-full">
+						<label className="block font-medium mb-1">{t("settings:providers.codestralBaseUrl")}</label>
+					</VSCodeTextField>
+					<div className="text-sm text-vscode-descriptionForeground -mt-2">
+						{t("settings:providers.codestralBaseUrlDesc")}
+					</div>
+				</>
+			)}
+		</>
+	)
+}

+ 64 - 1
webview-ui/src/components/settings/providers/OpenRouter.tsx

@@ -2,20 +2,29 @@ import { useCallback, useState } from "react"
 import { Trans } from "react-i18next"
 import { Checkbox } from "vscrui"
 import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
+import { ExternalLinkIcon } from "@radix-ui/react-icons"
 
-import { ApiConfiguration } from "@roo/shared/api"
+import { ApiConfiguration, RouterModels, openRouterDefaultModelId } from "@roo/shared/api"
 
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { getOpenRouterAuthUrl } from "@src/oauth/urls"
+import {
+	useOpenRouterModelProviders,
+	OPENROUTER_DEFAULT_PROVIDER_NAME,
+} from "@src/components/ui/hooks/useOpenRouterModelProviders"
 import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
 
 import { inputEventTransform, noTransform } from "../transforms"
 
+import { ModelPicker } from "../ModelPicker"
 import { OpenRouterBalanceDisplay } from "./OpenRouterBalanceDisplay"
 
 type OpenRouterProps = {
 	apiConfiguration: ApiConfiguration
 	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	routerModels?: RouterModels
+	selectedModelId: string
 	uriScheme: string | undefined
 	fromWelcomeView?: boolean
 }
@@ -23,6 +32,8 @@ type OpenRouterProps = {
 export const OpenRouter = ({
 	apiConfiguration,
 	setApiConfigurationField,
+	routerModels,
+	selectedModelId,
 	uriScheme,
 	fromWelcomeView,
 }: OpenRouterProps) => {
@@ -41,6 +52,14 @@ export const OpenRouter = ({
 		[setApiConfigurationField],
 	)
 
+	const { data: openRouterModelProviders } = useOpenRouterModelProviders(apiConfiguration?.openRouterModelId, {
+		enabled:
+			!!apiConfiguration?.openRouterModelId &&
+			routerModels?.openrouter &&
+			Object.keys(routerModels.openrouter).length > 1 &&
+			apiConfiguration.openRouterModelId in routerModels.openrouter,
+	})
+
 	return (
 		<>
 			<VSCodeTextField
@@ -104,6 +123,50 @@ export const OpenRouter = ({
 					</Checkbox>
 				</>
 			)}
+			<ModelPicker
+				apiConfiguration={apiConfiguration}
+				setApiConfigurationField={setApiConfigurationField}
+				defaultModelId={openRouterDefaultModelId}
+				models={routerModels?.openrouter ?? {}}
+				modelIdKey="openRouterModelId"
+				serviceName="OpenRouter"
+				serviceUrl="https://openrouter.ai/models"
+			/>
+			{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>
+			)}
 		</>
 	)
 }

+ 97 - 0
webview-ui/src/components/settings/providers/Requesty.tsx

@@ -0,0 +1,97 @@
+import { useCallback, useState } from "react"
+import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
+
+import { ApiConfiguration, RouterModels, requestyDefaultModelId } from "@roo/shared/api"
+
+import { vscode } from "@src/utils/vscode"
+import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
+import { Button } from "@src/components/ui"
+
+import { inputEventTransform } from "../transforms"
+import { ModelPicker } from "../ModelPicker"
+import { RequestyBalanceDisplay } from "./RequestyBalanceDisplay"
+
+type RequestyProps = {
+	apiConfiguration: ApiConfiguration
+	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	routerModels?: RouterModels
+	refetchRouterModels: () => void
+}
+
+export const Requesty = ({
+	apiConfiguration,
+	setApiConfigurationField,
+	routerModels,
+	refetchRouterModels,
+}: RequestyProps) => {
+	const { t } = useAppTranslation()
+
+	const [didRefetch, setDidRefetch] = useState<boolean>()
+
+	const handleInputChange = useCallback(
+		<K extends keyof ApiConfiguration, E>(
+			field: K,
+			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+		) =>
+			(event: E | Event) => {
+				setApiConfigurationField(field, transform(event as E))
+			},
+		[setApiConfigurationField],
+	)
+
+	return (
+		<>
+			<VSCodeTextField
+				value={apiConfiguration?.requestyApiKey || ""}
+				type="password"
+				onInput={handleInputChange("requestyApiKey")}
+				placeholder={t("settings:providers.getRequestyApiKey")}
+				className="w-full">
+				<div className="flex justify-between items-center mb-1">
+					<label className="block font-medium">{t("settings:providers.requestyApiKey")}</label>
+					{apiConfiguration?.requestyApiKey && (
+						<RequestyBalanceDisplay apiKey={apiConfiguration.requestyApiKey} />
+					)}
+				</div>
+			</VSCodeTextField>
+			<div className="text-sm text-vscode-descriptionForeground -mt-2">
+				{t("settings:providers.apiKeyStorageNotice")}
+			</div>
+			{!apiConfiguration?.requestyApiKey && (
+				<VSCodeButtonLink
+					href="https://app.requesty.ai/api-keys"
+					style={{ width: "100%" }}
+					appearance="primary">
+					{t("settings:providers.getRequestyApiKey")}
+				</VSCodeButtonLink>
+			)}
+			<Button
+				variant="outline"
+				onClick={() => {
+					vscode.postMessage({ type: "flushRouterModels", text: "requesty" })
+					refetchRouterModels()
+					setDidRefetch(true)
+				}}>
+				<div className="flex items-center gap-2">
+					<span className="codicon codicon-refresh" />
+					{t("settings:providers.refreshModels.label")}
+				</div>
+			</Button>
+			{didRefetch && (
+				<div className="flex items-center text-vscode-errorForeground">
+					{t("settings:providers.refreshModels.hint")}
+				</div>
+			)}
+			<ModelPicker
+				apiConfiguration={apiConfiguration}
+				setApiConfigurationField={setApiConfigurationField}
+				defaultModelId={requestyDefaultModelId}
+				models={routerModels?.requesty ?? {}}
+				modelIdKey="requestyModelId"
+				serviceName="Requesty"
+				serviceUrl="https://requesty.ai"
+			/>
+		</>
+	)
+}

+ 0 - 0
webview-ui/src/components/settings/RequestyBalanceDisplay.tsx → webview-ui/src/components/settings/providers/RequestyBalanceDisplay.tsx


+ 61 - 0
webview-ui/src/components/settings/providers/Unbound.tsx

@@ -0,0 +1,61 @@
+import { useCallback } from "react"
+import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
+
+import { ApiConfiguration, RouterModels, unboundDefaultModelId } from "@roo/shared/api"
+
+import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
+
+import { inputEventTransform } from "../transforms"
+import { ModelPicker } from "../ModelPicker"
+
+type UnboundProps = {
+	apiConfiguration: ApiConfiguration
+	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	routerModels?: RouterModels
+}
+
+export const Unbound = ({ apiConfiguration, setApiConfigurationField, routerModels }: UnboundProps) => {
+	const { t } = useAppTranslation()
+
+	const handleInputChange = useCallback(
+		<K extends keyof ApiConfiguration, E>(
+			field: K,
+			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+		) =>
+			(event: E | Event) => {
+				setApiConfigurationField(field, transform(event as E))
+			},
+		[setApiConfigurationField],
+	)
+
+	return (
+		<>
+			<VSCodeTextField
+				value={apiConfiguration?.unboundApiKey || ""}
+				type="password"
+				onInput={handleInputChange("unboundApiKey")}
+				placeholder={t("settings:placeholders.apiKey")}
+				className="w-full">
+				<label className="block font-medium mb-1">{t("settings:providers.unboundApiKey")}</label>
+			</VSCodeTextField>
+			<div className="text-sm text-vscode-descriptionForeground -mt-2">
+				{t("settings:providers.apiKeyStorageNotice")}
+			</div>
+			{!apiConfiguration?.unboundApiKey && (
+				<VSCodeButtonLink href="https://gateway.getunbound.ai" appearance="secondary">
+					{t("settings:providers.getUnboundApiKey")}
+				</VSCodeButtonLink>
+			)}
+			<ModelPicker
+				apiConfiguration={apiConfiguration}
+				defaultModelId={unboundDefaultModelId}
+				models={routerModels?.unbound ?? {}}
+				modelIdKey="unboundModelId"
+				serviceName="Unbound"
+				serviceUrl="https://api.getunbound.ai/models"
+				setApiConfigurationField={setApiConfigurationField}
+			/>
+		</>
+	)
+}

+ 50 - 0
webview-ui/src/components/settings/providers/XAI.tsx

@@ -0,0 +1,50 @@
+import { useCallback } from "react"
+import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
+
+import { ApiConfiguration } from "@roo/shared/api"
+
+import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
+
+import { inputEventTransform } from "../transforms"
+
+type XAIProps = {
+	apiConfiguration: ApiConfiguration
+	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+}
+
+export const XAI = ({ apiConfiguration, setApiConfigurationField }: XAIProps) => {
+	const { t } = useAppTranslation()
+
+	const handleInputChange = useCallback(
+		<K extends keyof ApiConfiguration, E>(
+			field: K,
+			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+		) =>
+			(event: E | Event) => {
+				setApiConfigurationField(field, transform(event as E))
+			},
+		[setApiConfigurationField],
+	)
+
+	return (
+		<>
+			<VSCodeTextField
+				value={apiConfiguration?.xaiApiKey || ""}
+				type="password"
+				onInput={handleInputChange("xaiApiKey")}
+				placeholder={t("settings:placeholders.apiKey")}
+				className="w-full">
+				<label className="block font-medium mb-1">{t("settings:providers.xaiApiKey")}</label>
+			</VSCodeTextField>
+			<div className="text-sm text-vscode-descriptionForeground -mt-2">
+				{t("settings:providers.apiKeyStorageNotice")}
+			</div>
+			{!apiConfiguration?.xaiApiKey && (
+				<VSCodeButtonLink href="https://api.x.ai/docs" appearance="secondary">
+					{t("settings:providers.getXaiApiKey")}
+				</VSCodeButtonLink>
+			)}
+		</>
+	)
+}

+ 18 - 0
webview-ui/src/components/settings/providers/index.ts

@@ -0,0 +1,18 @@
+export { Anthropic } from "./Anthropic"
+export { Bedrock } from "./Bedrock"
+export { Chutes } from "./Chutes"
+export { DeepSeek } from "./DeepSeek"
+export { Gemini } from "./Gemini"
+export { Glama } from "./Glama"
+export { Groq } from "./Groq"
+export { LMStudio } from "./LMStudio"
+export { Mistral } from "./Mistral"
+export { Ollama } from "./Ollama"
+export { OpenAI } from "./OpenAI"
+export { OpenAICompatible } from "./OpenAICompatible"
+export { OpenRouter } from "./OpenRouter"
+export { Requesty } from "./Requesty"
+export { Unbound } from "./Unbound"
+export { Vertex } from "./Vertex"
+export { VSCodeLM } from "./VSCodeLM"
+export { XAI } from "./XAI"

+ 122 - 81
webview-ui/src/components/ui/hooks/useSelectedModel.ts

@@ -1,7 +1,8 @@
 import {
-	ApiConfiguration,
-	RouterModels,
-	ModelInfo,
+	type ProviderName,
+	type ApiConfiguration,
+	type RouterModels,
+	type ModelInfo,
 	anthropicDefaultModelId,
 	anthropicModels,
 	bedrockDefaultModelId,
@@ -36,97 +37,137 @@ import { useRouterModels } from "./useRouterModels"
 export const useSelectedModel = (apiConfiguration?: ApiConfiguration) => {
 	const { data: routerModels, isLoading, isError } = useRouterModels()
 	const provider = apiConfiguration?.apiProvider || "anthropic"
-	const id = apiConfiguration ? getSelectedModelId({ provider, apiConfiguration }) : anthropicDefaultModelId
-	const info = routerModels ? getSelectedModelInfo({ provider, id, apiConfiguration, routerModels }) : undefined
-	return { provider, id, info, isLoading, isError }
-}
 
-function getSelectedModelId({ provider, apiConfiguration }: { provider: string; apiConfiguration: ApiConfiguration }) {
-	switch (provider) {
-		case "openrouter":
-			return apiConfiguration.openRouterModelId ?? openRouterDefaultModelId
-		case "requesty":
-			return apiConfiguration.requestyModelId ?? requestyDefaultModelId
-		case "glama":
-			return apiConfiguration.glamaModelId ?? glamaDefaultModelId
-		case "unbound":
-			return apiConfiguration.unboundModelId ?? unboundDefaultModelId
-		case "openai":
-			return apiConfiguration.openAiModelId || ""
-		case "ollama":
-			return apiConfiguration.ollamaModelId || ""
-		case "lmstudio":
-			return apiConfiguration.lmStudioModelId || ""
-		case "vscode-lm":
-			return apiConfiguration?.vsCodeLmModelSelector
-				? `${apiConfiguration.vsCodeLmModelSelector.vendor}/${apiConfiguration.vsCodeLmModelSelector.family}`
-				: ""
-		default:
-			return apiConfiguration.apiModelId ?? anthropicDefaultModelId
-	}
+	const { id, info } =
+		apiConfiguration && routerModels
+			? getSelectedModel({ provider, apiConfiguration, routerModels })
+			: { id: anthropicDefaultModelId, info: undefined }
+
+	return { provider, id, info, isLoading, isError }
 }
 
-function getSelectedModelInfo({
+function getSelectedModel({
 	provider,
-	id,
 	apiConfiguration,
 	routerModels,
 }: {
-	provider: string
-	id: string
-	apiConfiguration?: ApiConfiguration
+	provider: ProviderName
+	apiConfiguration: ApiConfiguration
 	routerModels: RouterModels
-}): ModelInfo {
+}): { id: string; info: ModelInfo } {
 	switch (provider) {
-		case "openrouter":
-			return routerModels.openrouter[id] ?? routerModels.openrouter[openRouterDefaultModelId]
-		case "requesty":
-			return routerModels.requesty[id] ?? routerModels.requesty[requestyDefaultModelId]
-		case "glama":
-			return routerModels.glama[id] ?? routerModels.glama[glamaDefaultModelId]
-		case "unbound":
-			return routerModels.unbound[id] ?? routerModels.unbound[unboundDefaultModelId]
-		case "xai":
-			return xaiModels[id as keyof typeof xaiModels] ?? xaiModels[xaiDefaultModelId]
-		case "groq":
-			return groqModels[id as keyof typeof groqModels] ?? groqModels[groqDefaultModelId]
-		case "chutes":
-			return chutesModels[id as keyof typeof chutesModels] ?? chutesModels[chutesDefaultModelId]
-		case "bedrock":
+		case "openrouter": {
+			const id = apiConfiguration.openRouterModelId ?? openRouterDefaultModelId
+			const info = routerModels.openrouter[id]
+			return info
+				? { id, info }
+				: { id: openRouterDefaultModelId, info: routerModels.openrouter[openRouterDefaultModelId] }
+		}
+		case "requesty": {
+			const id = apiConfiguration.requestyModelId ?? requestyDefaultModelId
+			const info = routerModels.requesty[id]
+			return info
+				? { id, info }
+				: { id: requestyDefaultModelId, info: routerModels.requesty[requestyDefaultModelId] }
+		}
+		case "glama": {
+			const id = apiConfiguration.glamaModelId ?? glamaDefaultModelId
+			const info = routerModels.glama[id]
+			return info ? { id, info } : { id: glamaDefaultModelId, info: routerModels.glama[glamaDefaultModelId] }
+		}
+		case "unbound": {
+			const id = apiConfiguration.unboundModelId ?? unboundDefaultModelId
+			const info = routerModels.unbound[id]
+			return info
+				? { id, info }
+				: { id: unboundDefaultModelId, info: routerModels.unbound[unboundDefaultModelId] }
+		}
+		case "xai": {
+			const id = apiConfiguration.apiModelId ?? xaiDefaultModelId
+			const info = xaiModels[id as keyof typeof xaiModels]
+			return info ? { id, info } : { id: xaiDefaultModelId, info: xaiModels[xaiDefaultModelId] }
+		}
+		case "groq": {
+			const id = apiConfiguration.apiModelId ?? groqDefaultModelId
+			const info = groqModels[id as keyof typeof groqModels]
+			return info ? { id, info } : { id: groqDefaultModelId, info: groqModels[groqDefaultModelId] }
+		}
+		case "chutes": {
+			const id = apiConfiguration.apiModelId ?? chutesDefaultModelId
+			const info = chutesModels[id as keyof typeof chutesModels]
+			return info ? { id, info } : { id: chutesDefaultModelId, info: chutesModels[chutesDefaultModelId] }
+		}
+		case "bedrock": {
+			const id = apiConfiguration.apiModelId ?? bedrockDefaultModelId
+			const info = bedrockModels[id as keyof typeof bedrockModels]
+
 			// Special case for custom ARN.
 			if (id === "custom-arn") {
-				return { maxTokens: 5000, contextWindow: 128_000, supportsPromptCache: false, supportsImages: true }
+				return {
+					id,
+					info: { maxTokens: 5000, contextWindow: 128_000, supportsPromptCache: false, supportsImages: true },
+				}
 			}
 
-			return bedrockModels[id as keyof typeof bedrockModels] ?? bedrockModels[bedrockDefaultModelId]
-		case "vertex":
-			return vertexModels[id as keyof typeof vertexModels] ?? vertexModels[vertexDefaultModelId]
-		case "gemini":
-			return geminiModels[id as keyof typeof geminiModels] ?? geminiModels[geminiDefaultModelId]
-		case "deepseek":
-			return deepSeekModels[id as keyof typeof deepSeekModels] ?? deepSeekModels[deepSeekDefaultModelId]
-		case "openai-native":
-			return (
-				openAiNativeModels[id as keyof typeof openAiNativeModels] ??
-				openAiNativeModels[openAiNativeDefaultModelId]
-			)
-		case "mistral":
-			return mistralModels[id as keyof typeof mistralModels] ?? mistralModels[mistralDefaultModelId]
-		case "openai":
-			return apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults
-		case "ollama":
-			return openAiModelInfoSaneDefaults
-		case "lmstudio":
-			return openAiModelInfoSaneDefaults
-		case "vscode-lm":
+			return info ? { id, info } : { id: bedrockDefaultModelId, info: bedrockModels[bedrockDefaultModelId] }
+		}
+		case "vertex": {
+			const id = apiConfiguration.apiModelId ?? vertexDefaultModelId
+			const info = vertexModels[id as keyof typeof vertexModels]
+			return info ? { id, info } : { id: vertexDefaultModelId, info: vertexModels[vertexDefaultModelId] }
+		}
+		case "gemini": {
+			const id = apiConfiguration.apiModelId ?? geminiDefaultModelId
+			const info = geminiModels[id as keyof typeof geminiModels]
+			return info ? { id, info } : { id: geminiDefaultModelId, info: geminiModels[geminiDefaultModelId] }
+		}
+		case "deepseek": {
+			const id = apiConfiguration.apiModelId ?? deepSeekDefaultModelId
+			const info = deepSeekModels[id as keyof typeof deepSeekModels]
+			return info ? { id, info } : { id: deepSeekDefaultModelId, info: deepSeekModels[deepSeekDefaultModelId] }
+		}
+		case "openai-native": {
+			const id = apiConfiguration.apiModelId ?? openAiNativeDefaultModelId
+			const info = openAiNativeModels[id as keyof typeof openAiNativeModels]
+			return info
+				? { id, info }
+				: { id: openAiNativeDefaultModelId, info: openAiNativeModels[openAiNativeDefaultModelId] }
+		}
+		case "mistral": {
+			const id = apiConfiguration.apiModelId ?? mistralDefaultModelId
+			const info = mistralModels[id as keyof typeof mistralModels]
+			return info ? { id, info } : { id: mistralDefaultModelId, info: mistralModels[mistralDefaultModelId] }
+		}
+		case "openai": {
+			const id = apiConfiguration.openAiModelId ?? ""
+			const info = apiConfiguration?.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults
+			return { id, info }
+		}
+		case "ollama": {
+			const id = apiConfiguration.ollamaModelId ?? ""
+			const info = openAiModelInfoSaneDefaults
+			return { id, info }
+		}
+		case "lmstudio": {
+			const id = apiConfiguration.lmStudioModelId ?? ""
+			const info = openAiModelInfoSaneDefaults
+			return { id, info }
+		}
+		case "vscode-lm": {
+			const id = apiConfiguration?.vsCodeLmModelSelector
+				? `${apiConfiguration.vsCodeLmModelSelector.vendor}/${apiConfiguration.vsCodeLmModelSelector.family}`
+				: vscodeLlmDefaultModelId
 			const modelFamily = apiConfiguration?.vsCodeLmModelSelector?.family ?? vscodeLlmDefaultModelId
-
-			return {
-				...openAiModelInfoSaneDefaults,
-				...vscodeLlmModels[modelFamily as keyof typeof vscodeLlmModels],
-				supportsImages: false, // VSCode LM API currently doesn't support images.
-			}
-		default:
-			return anthropicModels[id as keyof typeof anthropicModels] ?? anthropicModels[anthropicDefaultModelId]
+			const info = vscodeLlmModels[modelFamily as keyof typeof vscodeLlmModels]
+			return { id, info: { ...openAiModelInfoSaneDefaults, ...info, supportsImages: false } } // VSCode LM API currently doesn't support images.
+		}
+		// case "anthropic":
+		// case "human-relay":
+		// case "fake-ai":
+		default: {
+			const id = apiConfiguration.apiModelId ?? anthropicDefaultModelId
+			const info = anthropicModels[id as keyof typeof anthropicModels]
+			return info ? { id, info } : { id: anthropicDefaultModelId, info: anthropicModels[anthropicDefaultModelId] }
+		}
 	}
 }

+ 4 - 2
webview-ui/src/i18n/locales/ca/settings.json

@@ -119,9 +119,11 @@
 		"glamaApiKey": "Clau API de Glama",
 		"getGlamaApiKey": "Obtenir clau API de Glama",
 		"requestyApiKey": "Clau API de Requesty",
-		"flushModelsCache": "Netejar memòria cau de models",
-		"flushedModelsCache": "Memòria cau netejada, si us plau torna a obrir la vista de configuració",
 		"getRequestyApiKey": "Obtenir clau API de Requesty",
+		"refreshModels": {
+			"label": "Actualitzar models",
+			"hint": "Si us plau, torneu a obrir la configuració per veure els models més recents."
+		},
 		"anthropicApiKey": "Clau API d'Anthropic",
 		"getAnthropicApiKey": "Obtenir clau API d'Anthropic",
 		"anthropicUseAuthToken": "Passar la clau API d'Anthropic com a capçalera d'autorització en lloc de X-Api-Key",

+ 4 - 2
webview-ui/src/i18n/locales/de/settings.json

@@ -106,8 +106,6 @@
 		"awsCustomArnUse": "Geben Sie eine gültige Amazon Bedrock ARN für das Modell ein, das Sie verwenden möchten. Formatbeispiele:",
 		"awsCustomArnDesc": "Stellen Sie sicher, dass die Region in der ARN mit Ihrer oben ausgewählten AWS-Region übereinstimmt.",
 		"openRouterApiKey": "OpenRouter API-Schlüssel",
-		"flushModelsCache": "Modell-Cache leeren",
-		"flushedModelsCache": "Cache geleert, bitte öffnen Sie die Einstellungsansicht erneut",
 		"getOpenRouterApiKey": "OpenRouter API-Schlüssel erhalten",
 		"apiKeyStorageNotice": "API-Schlüssel werden sicher im VSCode Secret Storage gespeichert",
 		"glamaApiKey": "Glama API-Schlüssel",
@@ -121,6 +119,10 @@
 		"noCustomHeaders": "Keine benutzerdefinierten Headers definiert. Klicke auf die + Schaltfläche, um einen hinzuzufügen.",
 		"requestyApiKey": "Requesty API-Schlüssel",
 		"getRequestyApiKey": "Requesty API-Schlüssel erhalten",
+		"refreshModels": {
+			"label": "Modelle aktualisieren",
+			"hint": "Bitte öffne die Einstellungen erneut, um die neuesten Modelle zu sehen."
+		},
 		"openRouterTransformsText": "Prompts und Nachrichtenketten auf Kontextgröße komprimieren (<a>OpenRouter Transformationen</a>)",
 		"anthropicApiKey": "Anthropic API-Schlüssel",
 		"getAnthropicApiKey": "Anthropic API-Schlüssel erhalten",

+ 4 - 2
webview-ui/src/i18n/locales/en/settings.json

@@ -118,8 +118,10 @@
 		"headerValue": "Header value",
 		"noCustomHeaders": "No custom headers defined. Click the + button to add one.",
 		"requestyApiKey": "Requesty API Key",
-		"flushModelsCache": "Flush cached models",
-		"flushedModelsCache": "Flushed cache, please reopen the settings view",
+		"refreshModels": {
+			"label": "Refresh Models",
+			"hint": "Please reopen the settings to see the latest models."
+		},
 		"getRequestyApiKey": "Get Requesty API Key",
 		"openRouterTransformsText": "Compress prompts and message chains to the context size (<a>OpenRouter Transforms</a>)",
 		"anthropicApiKey": "Anthropic API Key",

+ 4 - 2
webview-ui/src/i18n/locales/es/settings.json

@@ -106,8 +106,6 @@
 		"awsCustomArnUse": "Ingrese un ARN de Amazon Bedrock válido para el modelo que desea utilizar. Ejemplos de formato:",
 		"awsCustomArnDesc": "Asegúrese de que la región en el ARN coincida con la región de AWS seleccionada anteriormente.",
 		"openRouterApiKey": "Clave API de OpenRouter",
-		"flushModelsCache": "Limpiar modelos en caché",
-		"flushedModelsCache": "Caché limpiada, por favor vuelva a abrir la vista de configuración",
 		"getOpenRouterApiKey": "Obtener clave API de OpenRouter",
 		"apiKeyStorageNotice": "Las claves API se almacenan de forma segura en el Almacenamiento Secreto de VSCode",
 		"glamaApiKey": "Clave API de Glama",
@@ -121,6 +119,10 @@
 		"noCustomHeaders": "No hay encabezados personalizados definidos. Haga clic en el botón + para añadir uno.",
 		"requestyApiKey": "Clave API de Requesty",
 		"getRequestyApiKey": "Obtener clave API de Requesty",
+		"refreshModels": {
+			"label": "Actualizar modelos",
+			"hint": "Por favor, vuelve a abrir la configuración para ver los modelos más recientes."
+		},
 		"openRouterTransformsText": "Comprimir prompts y cadenas de mensajes al tamaño del contexto (<a>Transformaciones de OpenRouter</a>)",
 		"anthropicApiKey": "Clave API de Anthropic",
 		"getAnthropicApiKey": "Obtener clave API de Anthropic",

+ 4 - 2
webview-ui/src/i18n/locales/fr/settings.json

@@ -106,8 +106,6 @@
 		"awsCustomArnUse": "Entrez un ARN Amazon Bedrock valide pour le modèle que vous souhaitez utiliser. Exemples de format :",
 		"awsCustomArnDesc": "Assurez-vous que la région dans l'ARN correspond à la région AWS sélectionnée ci-dessus.",
 		"openRouterApiKey": "Clé API OpenRouter",
-		"flushModelsCache": "Vider le cache des modèles",
-		"flushedModelsCache": "Cache vidé, veuillez rouvrir la vue des paramètres",
 		"getOpenRouterApiKey": "Obtenir la clé API OpenRouter",
 		"apiKeyStorageNotice": "Les clés API sont stockées en toute sécurité dans le stockage sécurisé de VSCode",
 		"glamaApiKey": "Clé API Glama",
@@ -121,6 +119,10 @@
 		"noCustomHeaders": "Aucun en-tête personnalisé défini. Cliquez sur le bouton + pour en ajouter un.",
 		"requestyApiKey": "Clé API Requesty",
 		"getRequestyApiKey": "Obtenir la clé API Requesty",
+		"refreshModels": {
+			"label": "Actualiser les modèles",
+			"hint": "Veuillez rouvrir les paramètres pour voir les modèles les plus récents."
+		},
 		"openRouterTransformsText": "Compresser les prompts et chaînes de messages à la taille du contexte (<a>Transformations OpenRouter</a>)",
 		"anthropicApiKey": "Clé API Anthropic",
 		"getAnthropicApiKey": "Obtenir la clé API Anthropic",

+ 4 - 2
webview-ui/src/i18n/locales/hi/settings.json

@@ -118,9 +118,11 @@
 		"headerValue": "हेडर मूल्य",
 		"noCustomHeaders": "कोई कस्टम हेडर परिभाषित नहीं है। एक जोड़ने के लिए + बटन पर क्लिक करें।",
 		"requestyApiKey": "Requesty API कुंजी",
-		"flushModelsCache": "मॉडल कैश साफ़ करें",
-		"flushedModelsCache": "कैश साफ़ किया गया, कृपया सेटिंग्स व्यू को फिर से खोलें",
 		"getRequestyApiKey": "Requesty API कुंजी प्राप्त करें",
+		"refreshModels": {
+			"label": "मॉडल रिफ्रेश करें",
+			"hint": "नवीनतम मॉडल देखने के लिए कृपया सेटिंग्स को फिर से खोलें।"
+		},
 		"openRouterTransformsText": "संदर्भ आकार के लिए प्रॉम्प्ट और संदेश श्रृंखलाओं को संपीड़ित करें (<a>OpenRouter ट्रांसफॉर्म</a>)",
 		"anthropicApiKey": "Anthropic API कुंजी",
 		"getAnthropicApiKey": "Anthropic API कुंजी प्राप्त करें",

+ 4 - 2
webview-ui/src/i18n/locales/it/settings.json

@@ -118,9 +118,11 @@
 		"headerValue": "Valore intestazione",
 		"noCustomHeaders": "Nessuna intestazione personalizzata definita. Fai clic sul pulsante + per aggiungerne una.",
 		"requestyApiKey": "Chiave API Requesty",
-		"flushModelsCache": "Svuota cache dei modelli",
-		"flushedModelsCache": "Cache svuotata, riapri la vista delle impostazioni",
 		"getRequestyApiKey": "Ottieni chiave API Requesty",
+		"refreshModels": {
+			"label": "Aggiorna modelli",
+			"hint": "Riapri le impostazioni per vedere i modelli più recenti."
+		},
 		"openRouterTransformsText": "Comprimi prompt e catene di messaggi alla dimensione del contesto (<a>Trasformazioni OpenRouter</a>)",
 		"anthropicApiKey": "Chiave API Anthropic",
 		"getAnthropicApiKey": "Ottieni chiave API Anthropic",

+ 4 - 2
webview-ui/src/i18n/locales/ja/settings.json

@@ -118,9 +118,11 @@
 		"headerValue": "ヘッダー値",
 		"noCustomHeaders": "カスタムヘッダーが定義されていません。+ ボタンをクリックして追加してください。",
 		"requestyApiKey": "Requesty APIキー",
-		"flushModelsCache": "モデルキャッシュをクリア",
-		"flushedModelsCache": "キャッシュをクリアしました。設定ビューを再開してください",
 		"getRequestyApiKey": "Requesty APIキーを取得",
+		"refreshModels": {
+			"label": "モデルを更新",
+			"hint": "最新のモデルを表示するには設定を再度開いてください。"
+		},
 		"openRouterTransformsText": "プロンプトとメッセージチェーンをコンテキストサイズに圧縮 (<a>OpenRouter Transforms</a>)",
 		"anthropicApiKey": "Anthropic APIキー",
 		"getAnthropicApiKey": "Anthropic APIキーを取得",

+ 4 - 2
webview-ui/src/i18n/locales/ko/settings.json

@@ -118,9 +118,11 @@
 		"headerValue": "헤더 값",
 		"noCustomHeaders": "정의된 사용자 정의 헤더가 없습니다. + 버튼을 클릭하여 추가하세요.",
 		"requestyApiKey": "Requesty API 키",
-		"flushModelsCache": "모델 캐시 지우기",
-		"flushedModelsCache": "캐시가 지워졌습니다. 설정 보기를 다시 열어주세요",
 		"getRequestyApiKey": "Requesty API 키 받기",
+		"refreshModels": {
+			"label": "모델 새로고침",
+			"hint": "최신 모델을 보려면 설정을 다시 열어주세요."
+		},
 		"openRouterTransformsText": "프롬프트와 메시지 체인을 컨텍스트 크기로 압축 (<a>OpenRouter Transforms</a>)",
 		"anthropicApiKey": "Anthropic API 키",
 		"getAnthropicApiKey": "Anthropic API 키 받기",

+ 4 - 2
webview-ui/src/i18n/locales/pl/settings.json

@@ -118,9 +118,11 @@
 		"headerValue": "Wartość nagłówka",
 		"noCustomHeaders": "Brak zdefiniowanych niestandardowych nagłówków. Kliknij przycisk +, aby dodać.",
 		"requestyApiKey": "Klucz API Requesty",
-		"flushModelsCache": "Wyczyść pamięć podręczną modeli",
-		"flushedModelsCache": "Pamięć podręczna wyczyszczona, proszę ponownie otworzyć widok ustawień",
 		"getRequestyApiKey": "Uzyskaj klucz API Requesty",
+		"refreshModels": {
+			"label": "Odśwież modele",
+			"hint": "Proszę ponownie otworzyć ustawienia, aby zobaczyć najnowsze modele."
+		},
 		"openRouterTransformsText": "Kompresuj podpowiedzi i łańcuchy wiadomości do rozmiaru kontekstu (<a>Transformacje OpenRouter</a>)",
 		"anthropicApiKey": "Klucz API Anthropic",
 		"getAnthropicApiKey": "Uzyskaj klucz API Anthropic",

+ 4 - 2
webview-ui/src/i18n/locales/pt-BR/settings.json

@@ -106,8 +106,6 @@
 		"awsCustomArnUse": "Insira um ARN Amazon Bedrock válido para o modelo que deseja usar. Exemplos de formato:",
 		"awsCustomArnDesc": "Certifique-se de que a região no ARN corresponde à região AWS selecionada acima.",
 		"openRouterApiKey": "Chave de API OpenRouter",
-		"flushModelsCache": "Limpar cache de modelos",
-		"flushedModelsCache": "Cache limpo, por favor reabra a visualização de configurações",
 		"getOpenRouterApiKey": "Obter chave de API OpenRouter",
 		"apiKeyStorageNotice": "As chaves de API são armazenadas com segurança no Armazenamento Secreto do VSCode",
 		"glamaApiKey": "Chave de API Glama",
@@ -121,6 +119,10 @@
 		"noCustomHeaders": "Nenhum cabeçalho personalizado definido. Clique no botão + para adicionar um.",
 		"requestyApiKey": "Chave de API Requesty",
 		"getRequestyApiKey": "Obter chave de API Requesty",
+		"refreshModels": {
+			"label": "Atualizar modelos",
+			"hint": "Por favor, reabra as configurações para ver os modelos mais recentes."
+		},
 		"openRouterTransformsText": "Comprimir prompts e cadeias de mensagens para o tamanho do contexto (<a>Transformações OpenRouter</a>)",
 		"anthropicApiKey": "Chave de API Anthropic",
 		"getAnthropicApiKey": "Obter chave de API Anthropic",

+ 4 - 2
webview-ui/src/i18n/locales/ru/settings.json

@@ -118,9 +118,11 @@
 		"headerValue": "Значение заголовка",
 		"noCustomHeaders": "Пользовательские заголовки не определены. Нажмите кнопку +, чтобы добавить.",
 		"requestyApiKey": "Requesty API-ключ",
-		"flushModelsCache": "Очистить кэш моделей",
-		"flushedModelsCache": "Кэш очищен, пожалуйста, переоткройте представление настроек",
 		"getRequestyApiKey": "Получить Requesty API-ключ",
+		"refreshModels": {
+			"label": "Обновить модели",
+			"hint": "Пожалуйста, откройте настройки заново, чтобы увидеть последние модели."
+		},
 		"openRouterTransformsText": "Сжимать подсказки и цепочки сообщений до размера контекста (<a>OpenRouter Transforms</a>)",
 		"anthropicApiKey": "Anthropic API-ключ",
 		"getAnthropicApiKey": "Получить Anthropic API-ключ",

+ 4 - 2
webview-ui/src/i18n/locales/tr/settings.json

@@ -106,8 +106,6 @@
 		"awsCustomArnUse": "Kullanmak istediğiniz model için geçerli bir Amazon Bedrock ARN'si girin. Format örnekleri:",
 		"awsCustomArnDesc": "ARN içindeki bölgenin yukarıda seçilen AWS Bölgesiyle eşleştiğinden emin olun.",
 		"openRouterApiKey": "OpenRouter API Anahtarı",
-		"flushModelsCache": "Model önbelleğini temizle",
-		"flushedModelsCache": "Önbellek temizlendi, lütfen ayarlar görünümünü yeniden açın",
 		"getOpenRouterApiKey": "OpenRouter API Anahtarı Al",
 		"apiKeyStorageNotice": "API anahtarları VSCode'un Gizli Depolamasında güvenli bir şekilde saklanır",
 		"glamaApiKey": "Glama API Anahtarı",
@@ -121,6 +119,10 @@
 		"noCustomHeaders": "Tanımlanmış özel başlık yok. Eklemek için + düğmesine tıklayın.",
 		"requestyApiKey": "Requesty API Anahtarı",
 		"getRequestyApiKey": "Requesty API Anahtarı Al",
+		"refreshModels": {
+			"label": "Modelleri Yenile",
+			"hint": "En son modelleri görmek için lütfen ayarları yeniden açın."
+		},
 		"openRouterTransformsText": "İstem ve mesaj zincirlerini bağlam boyutuna sıkıştır (<a>OpenRouter Dönüşümleri</a>)",
 		"anthropicApiKey": "Anthropic API Anahtarı",
 		"getAnthropicApiKey": "Anthropic API Anahtarı Al",

+ 4 - 2
webview-ui/src/i18n/locales/vi/settings.json

@@ -118,8 +118,6 @@
 		"headerValue": "Giá trị tiêu đề",
 		"noCustomHeaders": "Chưa có tiêu đề tùy chỉnh nào được định nghĩa. Nhấp vào nút + để thêm.",
 		"requestyApiKey": "Khóa API Requesty",
-		"flushModelsCache": "Xóa bộ nhớ đệm mô hình",
-		"flushedModelsCache": "Đã xóa bộ nhớ đệm, vui lòng mở lại chế độ xem cài đặt",
 		"getRequestyApiKey": "Lấy khóa API Requesty",
 		"anthropicApiKey": "Khóa API Anthropic",
 		"getAnthropicApiKey": "Lấy khóa API Anthropic",
@@ -181,6 +179,10 @@
 			"warning": "Lưu ý: Roo Code sử dụng các lời nhắc phức tạp và hoạt động tốt nhất với các mô hình Claude. Các mô hình kém mạnh hơn có thể không hoạt động như mong đợi."
 		},
 		"openRouterTransformsText": "Nén lời nhắc và chuỗi tin nhắn theo kích thước ngữ cảnh (<a>OpenRouter Transforms</a>)",
+		"refreshModels": {
+			"label": "Làm mới mô hình",
+			"hint": "Vui lòng mở lại cài đặt để xem các mô hình mới nhất."
+		},
 		"unboundApiKey": "Khóa API Unbound",
 		"getUnboundApiKey": "Lấy khóa API Unbound",
 		"humanRelay": {

+ 4 - 2
webview-ui/src/i18n/locales/zh-CN/settings.json

@@ -118,9 +118,11 @@
 		"glamaApiKey": "Glama API 密钥",
 		"getGlamaApiKey": "获取 Glama API 密钥",
 		"requestyApiKey": "Requesty API 密钥",
-		"flushModelsCache": "清除模型缓存",
-		"flushedModelsCache": "缓存已清除,请重新打开设置视图",
 		"getRequestyApiKey": "获取 Requesty API 密钥",
+		"refreshModels": {
+			"label": "刷新模型",
+			"hint": "请重新打开设置以查看最新模型。"
+		},
 		"openRouterTransformsText": "自动压缩提示词和消息链到上下文长度限制内 (<a>OpenRouter转换</a>)",
 		"anthropicApiKey": "Anthropic API 密钥",
 		"getAnthropicApiKey": "获取 Anthropic API 密钥",

+ 4 - 2
webview-ui/src/i18n/locales/zh-TW/settings.json

@@ -118,9 +118,11 @@
 		"headerValue": "標頭值",
 		"noCustomHeaders": "尚未定義自訂標頭。點擊 + 按鈕以新增。",
 		"requestyApiKey": "Requesty API 金鑰",
-		"flushModelsCache": "清除模型快取",
-		"flushedModelsCache": "快取已清除,請重新開啟設定視圖",
 		"getRequestyApiKey": "取得 Requesty API 金鑰",
+		"refreshModels": {
+			"label": "重新整理模型",
+			"hint": "請重新開啟設定以查看最新模型。"
+		},
 		"openRouterTransformsText": "將提示和訊息鏈壓縮到上下文大小 (<a>OpenRouter 轉換</a>)",
 		"anthropicApiKey": "Anthropic API 金鑰",
 		"getAnthropicApiKey": "取得 Anthropic API 金鑰",

+ 1 - 6
webview-ui/src/oauth/urls.ts

@@ -1,6 +1,5 @@
 export function getCallbackUrl(provider: string, uriScheme?: string) {
-	const callbackUrl = `${uriScheme || "vscode"}://rooveterinaryinc.roo-cline/${provider}`
-	return encodeURIComponent(callbackUrl)
+	return encodeURIComponent(`${uriScheme || "vscode"}://rooveterinaryinc.roo-cline/${provider}`)
 }
 
 export function getGlamaAuthUrl(uriScheme?: string) {
@@ -14,7 +13,3 @@ export function getOpenRouterAuthUrl(uriScheme?: string) {
 export function getRequestyAuthUrl(uriScheme?: string) {
 	return `https://app.requesty.ai/oauth/authorize?callback_url=${getCallbackUrl("requesty", uriScheme)}`
 }
-
-export function getRequestyApiKeyUrl() {
-	return "https://app.requesty.ai/api-keys"
-}