diff --git a/.changeset/real-ties-destroy.md b/.changeset/real-ties-destroy.md new file mode 100644 index 00000000000..a2e9ba8eb04 --- /dev/null +++ b/.changeset/real-ties-destroy.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Fix model picker diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts index 2151a7172d1..5e570ca2a2b 100644 --- a/src/api/providers/requesty.ts +++ b/src/api/providers/requesty.ts @@ -42,26 +42,33 @@ export class RequestyHandler extends OpenAiHandler { } } -export async function getRequestyModels({ apiKey }: { apiKey?: string }) { +export async function getRequestyModels() { const models: Record = {} - if (!apiKey) { - return models - } - try { - const config: Record = {} - config["headers"] = { Authorization: `Bearer ${apiKey}` } - - const response = await axios.get("https://router.requesty.ai/v1/models", config) + const response = await axios.get("https://router.requesty.ai/v1/models") const rawModels = response.data.data for (const rawModel of rawModels) { + // { + // id: "anthropic/claude-3-5-sonnet-20240620", + // object: "model", + // created: 1740552655, + // owned_by: "system", + // input_price: 0.0000028, + // caching_price: 0.00000375, + // cached_price: 3e-7, + // output_price: 0.000015, + // max_output_tokens: 8192, + // context_window: 200000, + // supports_caching: true, + // description: + // "Anthropic's previous most intelligent model. High level of intelligence and capability. Excells in coding.", + // } + const modelInfo: ModelInfo = { maxTokens: rawModel.max_output_tokens, contextWindow: rawModel.context_window, - supportsImages: rawModel.support_image, - supportsComputerUse: rawModel.support_computer_use, supportsPromptCache: rawModel.supports_caching, inputPrice: parseApiPrice(rawModel.input_price), outputPrice: parseApiPrice(rawModel.output_price), @@ -72,8 +79,15 @@ export async function getRequestyModels({ apiKey }: { apiKey?: string }) { switch (rawModel.id) { case rawModel.id.startsWith("anthropic/claude-3-7-sonnet"): + modelInfo.supportsComputerUse = true + modelInfo.supportsImages = true modelInfo.maxTokens = 16384 break + case rawModel.id.startsWith("anthropic/claude-3-5-sonnet-20241022"): + modelInfo.supportsComputerUse = true + modelInfo.supportsImages = true + modelInfo.maxTokens = 8192 + break case rawModel.id.startsWith("anthropic/"): modelInfo.maxTokens = 8192 break diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 118bbddcf55..bc6f4578683 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -644,9 +644,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { } }) - const requestyApiKey = await this.getSecret("requestyApiKey") - - getRequestyModels({ apiKey: requestyApiKey }).then(async (requestyModels) => { + getRequestyModels().then(async (requestyModels) => { if (Object.keys(requestyModels).length > 0) { await fs.writeFile( path.join(cacheDir, GlobalFileNames.requestyModels), @@ -838,17 +836,15 @@ export class ClineProvider implements vscode.WebviewViewProvider { break case "refreshRequestyModels": - if (message?.values?.apiKey) { - const requestyModels = await getRequestyModels({ apiKey: message.values.apiKey }) + const requestyModels = await getRequestyModels() - if (Object.keys(requestyModels).length > 0) { - const cacheDir = await this.ensureCacheDirectoryExists() - await fs.writeFile( - path.join(cacheDir, GlobalFileNames.requestyModels), - JSON.stringify(requestyModels), - ) - await this.postMessageToWebview({ type: "requestyModels", requestyModels }) - } + if (Object.keys(requestyModels).length > 0) { + const cacheDir = await this.ensureCacheDirectoryExists() + await fs.writeFile( + path.join(cacheDir, GlobalFileNames.requestyModels), + JSON.stringify(requestyModels), + ) + await this.postMessageToWebview({ type: "requestyModels", requestyModels }) } break diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 8f64a9ba056..e87edffed16 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -27,10 +27,11 @@ export interface ExtensionMessage { | "workspaceUpdated" | "invoke" | "partialMessage" - | "glamaModels" | "openRouterModels" - | "openAiModels" + | "glamaModels" + | "unboundModels" | "requestyModels" + | "openAiModels" | "mcpServers" | "enhancedPrompt" | "commitSearchResults" @@ -43,8 +44,6 @@ export interface ExtensionMessage { | "autoApprovalEnabled" | "updateCustomMode" | "deleteCustomMode" - | "unboundModels" - | "refreshUnboundModels" | "currentCheckpointUpdated" text?: string action?: @@ -67,11 +66,11 @@ export interface ExtensionMessage { path?: string }> partialMessage?: ClineMessage + openRouterModels?: Record glamaModels?: Record + unboundModels?: Record requestyModels?: Record - openRouterModels?: Record openAiModels?: string[] - unboundModels?: Record mcpServers?: McpServer[] commits?: GitCommit[] listApiConfig?: ApiConfigMeta[] diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 106e6d243b9..fde7442cc1d 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -40,11 +40,11 @@ export interface WebviewMessage { | "openFile" | "openMention" | "cancelTask" - | "refreshGlamaModels" | "refreshOpenRouterModels" - | "refreshOpenAiModels" + | "refreshGlamaModels" | "refreshUnboundModels" | "refreshRequestyModels" + | "refreshOpenAiModels" | "alwaysAllowBrowser" | "alwaysAllowMcp" | "alwaysAllowModeSwitch" @@ -71,7 +71,6 @@ export interface WebviewMessage { | "mcpEnabled" | "enableMcpServerCreation" | "searchCommits" - | "refreshGlamaModels" | "alwaysApproveResubmit" | "requestDelaySeconds" | "rateLimitSeconds" diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index 1d64f934dc2..22564d01a65 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -3674,6 +3674,7 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz", "integrity": "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", @@ -4719,6 +4720,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, diff --git a/webview-ui/src/components/settings/ApiErrorMessage.tsx b/webview-ui/src/components/settings/ApiErrorMessage.tsx index 4b419957b6c..06764a1bfa0 100644 --- a/webview-ui/src/components/settings/ApiErrorMessage.tsx +++ b/webview-ui/src/components/settings/ApiErrorMessage.tsx @@ -4,13 +4,13 @@ interface ApiErrorMessageProps { errorMessage: string | undefined children?: React.ReactNode } -const ApiErrorMessage = ({ errorMessage, children }: ApiErrorMessageProps) => { - return ( -
- - {errorMessage} - {children} + +export const ApiErrorMessage = ({ errorMessage, children }: ApiErrorMessageProps) => ( +
+
+
+
{errorMessage}
- ) -} -export default ApiErrorMessage + {children} +
+) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 107f2a483ae..c30035cef01 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -4,8 +4,6 @@ import { Checkbox, Dropdown, Pane, type DropdownOption } from "vscrui" import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import * as vscodemodels from "vscode" -import { Slider } from "@/components/ui" - import { ApiConfiguration, ModelInfo, @@ -33,7 +31,6 @@ import { unboundDefaultModelInfo, requestyDefaultModelId, requestyDefaultModelInfo, - THINKING_BUDGET, } from "../../../../src/shared/api" import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage" @@ -44,7 +41,18 @@ import { DROPDOWN_Z_INDEX } from "./styles" import { ModelPicker } from "./ModelPicker" import { TemperatureControl } from "./TemperatureControl" import { validateApiConfiguration, validateModelId } from "@/utils/validate" -import ApiErrorMessage from "./ApiErrorMessage" +import { ApiErrorMessage } from "./ApiErrorMessage" +import { ThinkingBudget } from "./ThinkingBudget" + +const modelsByProvider: Record> = { + anthropic: anthropicModels, + bedrock: bedrockModels, + vertex: vertexModels, + gemini: geminiModels, + "openai-native": openAiNativeModels, + deepseek: deepSeekModels, + mistral: mistralModels, +} interface ApiOptionsProps { uriScheme: string | undefined @@ -66,18 +74,23 @@ const ApiOptions = ({ const [ollamaModels, setOllamaModels] = useState([]) const [lmStudioModels, setLmStudioModels] = useState([]) const [vsCodeLmModels, setVsCodeLmModels] = useState([]) + const [openRouterModels, setOpenRouterModels] = useState>({ [openRouterDefaultModelId]: openRouterDefaultModelInfo, }) + const [glamaModels, setGlamaModels] = useState>({ [glamaDefaultModelId]: glamaDefaultModelInfo, }) + const [unboundModels, setUnboundModels] = useState>({ [unboundDefaultModelId]: unboundDefaultModelInfo, }) + const [requestyModels, setRequestyModels] = useState>({ [requestyDefaultModelId]: requestyDefaultModelInfo, }) + const [openAiModels, setOpenAiModels] = useState | null>(null) const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl) @@ -85,8 +98,6 @@ const ApiOptions = ({ const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl) const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) - const anthropicThinkingBudget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default - const noTransform = (value: T) => value const inputEventTransform = (event: E) => (event as { target: HTMLInputElement })?.target?.value as any const dropdownEventTransform = (event: DropdownOption | string | undefined) => @@ -103,62 +114,87 @@ const ApiOptions = ({ [setApiConfigurationField], ) - const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => { - return normalizeApiConfiguration(apiConfiguration) - }, [apiConfiguration]) + const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo( + () => normalizeApiConfiguration(apiConfiguration), + [apiConfiguration], + ) - // Pull ollama/lmstudio models - // Debounced model updates, only executed 250ms after the user stops typing + // Debounced refresh model updates, only executed 250ms after the user + // stops typing. useDebounce( () => { - if (selectedProvider === "ollama") { - vscode.postMessage({ type: "requestOllamaModels", text: apiConfiguration?.ollamaBaseUrl }) - } else if (selectedProvider === "lmstudio") { - vscode.postMessage({ type: "requestLmStudioModels", text: apiConfiguration?.lmStudioBaseUrl }) - } else if (selectedProvider === "vscode-lm") { - vscode.postMessage({ type: "requestVsCodeLmModels" }) - } else if (selectedProvider === "openai") { - vscode.postMessage({ - type: "refreshOpenAiModels", - values: { - baseUrl: apiConfiguration?.openAiBaseUrl, - apiKey: apiConfiguration?.openAiApiKey, - }, - }) - } else if (selectedProvider === "openrouter") { - vscode.postMessage({ type: "refreshOpenRouterModels", values: {} }) + if (selectedProvider === "openrouter") { + vscode.postMessage({ type: "refreshOpenRouterModels" }) } else if (selectedProvider === "glama") { - vscode.postMessage({ type: "refreshGlamaModels", values: {} }) + vscode.postMessage({ type: "refreshGlamaModels" }) + } else if (selectedProvider === "unbound") { + vscode.postMessage({ type: "refreshUnboundModels" }) } else if (selectedProvider === "requesty") { vscode.postMessage({ type: "refreshRequestyModels", - values: { - apiKey: apiConfiguration?.requestyApiKey, - }, + values: { apiKey: apiConfiguration?.requestyApiKey }, + }) + } else if (selectedProvider === "openai") { + vscode.postMessage({ + type: "refreshOpenAiModels", + values: { baseUrl: apiConfiguration?.openAiBaseUrl, apiKey: apiConfiguration?.openAiApiKey }, }) + } else if (selectedProvider === "ollama") { + vscode.postMessage({ type: "requestOllamaModels", text: apiConfiguration?.ollamaBaseUrl }) + } else if (selectedProvider === "lmstudio") { + vscode.postMessage({ type: "requestLmStudioModels", text: apiConfiguration?.lmStudioBaseUrl }) + } else if (selectedProvider === "vscode-lm") { + vscode.postMessage({ type: "requestVsCodeLmModels" }) } }, 250, [ selectedProvider, - apiConfiguration?.ollamaBaseUrl, - apiConfiguration?.lmStudioBaseUrl, + apiConfiguration?.requestyApiKey, apiConfiguration?.openAiBaseUrl, apiConfiguration?.openAiApiKey, - apiConfiguration?.requestyApiKey, + apiConfiguration?.ollamaBaseUrl, + apiConfiguration?.lmStudioBaseUrl, ], ) useEffect(() => { const apiValidationResult = validateApiConfiguration(apiConfiguration) || - validateModelId(apiConfiguration, glamaModels, openRouterModels, unboundModels) + validateModelId(apiConfiguration, glamaModels, openRouterModels, unboundModels, requestyModels) + setErrorMessage(apiValidationResult) - }, [apiConfiguration, glamaModels, openRouterModels, setErrorMessage, unboundModels]) + }, [apiConfiguration, glamaModels, openRouterModels, setErrorMessage, unboundModels, requestyModels]) - const handleMessage = useCallback((event: MessageEvent) => { + const onMessage = useCallback((event: MessageEvent) => { const message: ExtensionMessage = event.data + switch (message.type) { + case "openRouterModels": { + const updatedModels = message.openRouterModels ?? {} + setOpenRouterModels({ [openRouterDefaultModelId]: openRouterDefaultModelInfo, ...updatedModels }) + break + } + case "glamaModels": { + const updatedModels = message.glamaModels ?? {} + setGlamaModels({ [glamaDefaultModelId]: glamaDefaultModelInfo, ...updatedModels }) + break + } + case "unboundModels": { + const updatedModels = message.unboundModels ?? {} + setUnboundModels({ [unboundDefaultModelId]: unboundDefaultModelInfo, ...updatedModels }) + break + } + case "requestyModels": { + const updatedModels = message.requestyModels ?? {} + setRequestyModels({ [requestyDefaultModelId]: requestyDefaultModelInfo, ...updatedModels }) + break + } + case "openAiModels": { + const updatedModels = message.openAiModels ?? [] + setOpenAiModels(Object.fromEntries(updatedModels.map((item) => [item, openAiModelInfoSaneDefaults]))) + break + } case "ollamaModels": { const newModels = message.ollamaModels ?? [] @@ -177,72 +213,30 @@ const ApiOptions = ({ setVsCodeLmModels(newModels) } break - case "glamaModels": { - const updatedModels = message.glamaModels ?? {} - setGlamaModels({ - [glamaDefaultModelId]: glamaDefaultModelInfo, // in case the extension sent a model list without the default model - ...updatedModels, - }) - break - } - case "openRouterModels": { - const updatedModels = message.openRouterModels ?? {} - setOpenRouterModels({ - [openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model - ...updatedModels, - }) - break - } - case "openAiModels": { - const updatedModels = message.openAiModels ?? [] - setOpenAiModels(Object.fromEntries(updatedModels.map((item) => [item, openAiModelInfoSaneDefaults]))) - break - } - case "unboundModels": { - const updatedModels = message.unboundModels ?? {} - setUnboundModels(updatedModels) - break - } - case "requestyModels": { - const updatedModels = message.requestyModels ?? {} - setRequestyModels({ - [requestyDefaultModelId]: requestyDefaultModelInfo, // in case the extension sent a model list without the default model - ...updatedModels, - }) - break - } } }, []) - useEvent("message", handleMessage) - - const createDropdown = (models: Record) => { - const options: DropdownOption[] = [ - { value: "", label: "Select a model..." }, - ...Object.keys(models).map((modelId) => ({ - value: modelId, - label: modelId, - })), - ] - - return ( - { - setApiConfigurationField("apiModelId", typeof value == "string" ? value : value?.value) - }} - style={{ width: "100%" }} - options={options} - /> - ) - } + useEvent("message", onMessage) + + const selectedProviderModelOptions: DropdownOption[] = useMemo( + () => + modelsByProvider[selectedProvider] + ? [ + { value: "", label: "Select a model..." }, + ...Object.keys(modelsByProvider[selectedProvider]).map((modelId) => ({ + value: modelId, + label: modelId, + })), + ] + : [], + [selectedProvider], + ) return (
-
+ {errorMessage && } + {selectedProvider === "anthropic" && (
- Anthropic API Key + Anthropic API Key - Glama API Key + Glama API Key {!apiConfiguration?.glamaApiKey && ( - Requesty API Key + Requesty API Key

- OpenAI API Key + OpenAI API Key

- Mistral API Key + Mistral API Key

- Codestral Base URL (Optional) + Codestral Base URL (Optional)

- OpenRouter API Key + OpenRouter API Key {!apiConfiguration?.openRouterApiKey && (

@@ -530,7 +526,7 @@ const ApiOptions = ({ style={{ width: "100%" }} onInput={handleInputChange("awsProfile")} placeholder="Enter profile name"> - AWS Profile Name + AWS Profile Name ) : ( <> @@ -541,7 +537,7 @@ const ApiOptions = ({ type="password" onInput={handleInputChange("awsAccessKey")} placeholder="Enter Access Key..."> - AWS Access Key + AWS Access Key - AWS Secret Key + AWS Secret Key - AWS Session Token + AWS Session Token )}

- Google Cloud Project ID + Google Cloud Project ID
- {errorMessage && }

- Gemini API Key + Gemini API Key

- Base URL + Base URL - API Key + API Key

)} - -
+
- Max Output Tokens + Max Output Tokens
- Context Window Size + Context Window Size
- Image Support + Image Support - Computer Use + Computer Use
- Input Price + Input Price
- Output Price + Output Price - Base URL (optional) + Base URL (optional) - Model ID + Model ID - {errorMessage && } - {lmStudioModels.length > 0 && ( {" "} feature to use it with this extension.{" "} - (Note: Roo Code uses complex prompts and works best + (Note: Roo Code uses complex prompts and works best with Claude models. Less capable models may not work as expected.)

@@ -1154,7 +1141,7 @@ const ApiOptions = ({ type="password" onInput={handleInputChange("deepSeekApiKey")} placeholder="Enter API Key..."> - DeepSeek API Key + DeepSeek API Key

{vsCodeLmModels.length > 0 ? ( - Base URL (optional) + Base URL (optional) - Model ID + Model ID {errorMessage && (
@@ -1284,7 +1271,7 @@ const ApiOptions = ({ quickstart guide. - (Note: Roo Code uses complex prompts and works best + (Note: Roo Code uses complex prompts and works best with Claude models. Less capable models may not work as expected.)

@@ -1299,7 +1286,7 @@ const ApiOptions = ({ type="password" onChange={handleInputChange("unboundApiKey")} placeholder="Enter API Key..."> - Unbound API Key + Unbound API Key {!apiConfiguration?.unboundApiKey && ( This key is stored locally and only used to make API requests from this extension.

-
)} + {selectedProvider === "openrouter" && ( + + )} + {selectedProvider === "glama" && ( )} - {selectedProvider === "openrouter" && ( + {selectedProvider === "unbound" && ( )} + {selectedProvider === "requesty" && ( )} - {selectedProvider !== "glama" && - selectedProvider !== "openrouter" && - selectedProvider !== "requesty" && - selectedProvider !== "openai" && - selectedProvider !== "ollama" && - selectedProvider !== "lmstudio" && - selectedProvider !== "unbound" && ( - <> -
- - {selectedProvider === "anthropic" && createDropdown(anthropicModels)} - {selectedProvider === "bedrock" && createDropdown(bedrockModels)} - {selectedProvider === "vertex" && createDropdown(vertexModels)} - {selectedProvider === "gemini" && createDropdown(geminiModels)} - {selectedProvider === "openai-native" && createDropdown(openAiNativeModels)} - {selectedProvider === "deepseek" && createDropdown(deepSeekModels)} - {selectedProvider === "mistral" && createDropdown(mistralModels)} -
- {errorMessage && } - - - )} - - {selectedModelInfo && selectedModelInfo.thinking && ( -
-
Thinking Budget
-
- setApiConfigurationField("anthropicThinking", value[0])} + {selectedProviderModelOptions.length > 0 && ( + <> +
+ + { + setApiConfigurationField("apiModelId", typeof value == "string" ? value : value?.value) + }} + options={selectedProviderModelOptions} + className="w-full" /> -
{anthropicThinkingBudget}
-
-
- Number of tokens Claude is allowed to use for its internal reasoning process.
-
+ + + )} {!fromWelcomeView && ( @@ -1459,6 +1423,7 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) { const getProviderData = (models: Record, defaultId: string) => { let selectedModelId: string let selectedModelInfo: ModelInfo + if (modelId && modelId in models) { selectedModelId = modelId selectedModelInfo = models[modelId] @@ -1466,8 +1431,10 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) { selectedModelId = defaultId selectedModelInfo = models[defaultId] } + return { selectedProvider: provider, selectedModelId, selectedModelInfo } } + switch (provider) { case "anthropic": return getProviderData(anthropicModels, anthropicDefaultModelId) @@ -1481,19 +1448,31 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) { return getProviderData(deepSeekModels, deepSeekDefaultModelId) case "openai-native": return getProviderData(openAiNativeModels, openAiNativeDefaultModelId) + case "mistral": + return getProviderData(mistralModels, mistralDefaultModelId) + case "openrouter": + return { + selectedProvider: provider, + selectedModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId, + selectedModelInfo: apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo, + } case "glama": return { selectedProvider: provider, selectedModelId: apiConfiguration?.glamaModelId || glamaDefaultModelId, selectedModelInfo: apiConfiguration?.glamaModelInfo || glamaDefaultModelInfo, } - case "mistral": - return getProviderData(mistralModels, mistralDefaultModelId) - case "openrouter": + case "unbound": return { selectedProvider: provider, - selectedModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId, - selectedModelInfo: apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo, + selectedModelId: apiConfiguration?.unboundModelId || unboundDefaultModelId, + selectedModelInfo: apiConfiguration?.unboundModelInfo || unboundDefaultModelInfo, + } + case "requesty": + return { + selectedProvider: provider, + selectedModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId, + selectedModelInfo: apiConfiguration?.requestyModelInfo || requestyDefaultModelInfo, } case "openai": return { @@ -1521,21 +1500,9 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) { : "", selectedModelInfo: { ...openAiModelInfoSaneDefaults, - supportsImages: false, // VSCode LM API currently doesn't support images + supportsImages: false, // VSCode LM API currently doesn't support images. }, } - case "unbound": - return { - selectedProvider: provider, - selectedModelId: apiConfiguration?.unboundModelId || unboundDefaultModelId, - selectedModelInfo: apiConfiguration?.unboundModelInfo || unboundDefaultModelInfo, - } - case "requesty": - return { - selectedProvider: provider, - selectedModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId, - selectedModelInfo: apiConfiguration?.requestyModelInfo || requestyDefaultModelInfo, - } default: return getProviderData(anthropicModels, anthropicDefaultModelId) } diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx index fd62bfb97b6..5a7737edd56 100644 --- a/webview-ui/src/components/settings/ModelPicker.tsx +++ b/webview-ui/src/components/settings/ModelPicker.tsx @@ -1,11 +1,13 @@ +import { useMemo, useState, useCallback, useEffect, useRef } from "react" import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" -import { useMemo, useState, useCallback, useEffect } from "react" + +import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem } from "@/components/ui/combobox" + +import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api" import { normalizeApiConfiguration } from "./ApiOptions" +import { ThinkingBudget } from "./ThinkingBudget" import { ModelInfoView } from "./ModelInfoView" -import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api" -import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem } from "../ui/combobox" -import ApiErrorMessage from "./ApiErrorMessage" type ExtractType = NonNullable< { [K in keyof ApiConfiguration]: Required[K] extends T ? K : never }[keyof ApiConfiguration] @@ -14,24 +16,17 @@ type ExtractType = NonNullable< type ModelIdKeys = NonNullable< { [K in keyof ApiConfiguration]: K extends `${string}ModelId` ? K : never }[keyof ApiConfiguration] > -declare module "react" { - interface CSSProperties { - // Allow CSS variables - [key: `--${string}`]: string | number - } -} + interface ModelPickerProps { - defaultModelId?: string + defaultModelId: string + defaultModelInfo?: ModelInfo models: Record | null modelIdKey: ModelIdKeys modelInfoKey: ExtractType serviceName: string serviceUrl: string - recommendedModel: string apiConfiguration: ApiConfiguration setApiConfigurationField: (field: K, value: ApiConfiguration[K]) => void - defaultModelInfo?: ModelInfo - errorMessage?: string } export const ModelPicker = ({ @@ -41,13 +36,12 @@ export const ModelPicker = ({ modelInfoKey, serviceName, serviceUrl, - recommendedModel, apiConfiguration, setApiConfigurationField, defaultModelInfo, - errorMessage, }: ModelPickerProps) => { const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) + const isInitialized = useRef(false) const modelIds = useMemo(() => Object.keys(models ?? {}).sort((a, b) => a.localeCompare(b)), [models]) @@ -55,6 +49,7 @@ export const ModelPicker = ({ () => normalizeApiConfiguration(apiConfiguration), [apiConfiguration], ) + const onSelect = useCallback( (modelId: string) => { const modelInfo = models?.[modelId] @@ -63,26 +58,23 @@ export const ModelPicker = ({ }, [modelIdKey, modelInfoKey, models, setApiConfigurationField, defaultModelInfo], ) + + const inputValue = apiConfiguration[modelIdKey] + useEffect(() => { - if (apiConfiguration[modelIdKey] == null && defaultModelId) { - onSelect(defaultModelId) + if (!inputValue && !isInitialized.current) { + const initialValue = modelIds.includes(selectedModelId) ? selectedModelId : defaultModelId + setApiConfigurationField(modelIdKey, initialValue) } - }, [apiConfiguration, defaultModelId, modelIdKey, onSelect]) + + isInitialized.current = true + }, [inputValue, modelIds, setApiConfigurationField, modelIdKey, selectedModelId, defaultModelId]) return ( <>
Model
- - + + No model found. {modelIds.map((model) => ( @@ -92,31 +84,18 @@ export const ModelPicker = ({ ))} - - {errorMessage ? ( - -

- - Note: Roo Code uses complex prompts and works best - with Claude models. Less capable models may not work as expected. - -

-
- ) : ( - selectedModelId && - selectedModelInfo && ( - - ) + + {selectedModelId && selectedModelInfo && selectedModelId === inputValue && ( + )}

The extension automatically fetches the latest list of models available on{" "} @@ -124,7 +103,7 @@ export const ModelPicker = ({ {serviceName}. If you're unsure which model to choose, Roo Code works best with{" "} - onSelect(recommendedModel)}>{recommendedModel}. + onSelect(defaultModelId)}>{defaultModelId}. You can also try searching "free" for no-cost options currently available.

diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index ee032c3ee06..d3e65a99ea8 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -66,21 +66,20 @@ const SettingsView = forwardRef(({ onDone }, terminalOutputLineLimit, writeDelayMs, } = cachedState - + //Make sure apiConfiguration is initialized and managed by SettingsView const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration]) useEffect(() => { - // Update only when currentApiConfigName is changed - // Expected to be triggered by loadApiConfiguration/upsertApiConfiguration + // Update only when currentApiConfigName is changed. + // Expected to be triggered by loadApiConfiguration/upsertApiConfiguration. if (prevApiConfigName.current === currentApiConfigName) { return } - setCachedState((prevCachedState) => ({ - ...prevCachedState, - ...extensionState, - })) + + setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState })) prevApiConfigName.current = currentApiConfigName + // console.log("useEffect: currentApiConfigName changed, setChangeDetected -> false") setChangeDetected(false) }, [currentApiConfigName, extensionState, isChangeDetected]) @@ -90,11 +89,10 @@ const SettingsView = forwardRef(({ onDone }, if (prevState[field] === value) { return prevState } + + // console.log(`setCachedStateField(${field} -> ${value}): setChangeDetected -> true`) setChangeDetected(true) - return { - ...prevState, - [field]: value, - } + return { ...prevState, [field]: value } }) }, [], @@ -107,15 +105,10 @@ const SettingsView = forwardRef(({ onDone }, return prevState } + // console.log(`setApiConfigurationField(${field} -> ${value}): setChangeDetected -> true`) setChangeDetected(true) - return { - ...prevState, - apiConfiguration: { - ...prevState.apiConfiguration, - [field]: value, - }, - } + return { ...prevState, apiConfiguration: { ...prevState.apiConfiguration, [field]: value } } }) }, [], @@ -126,14 +119,19 @@ const SettingsView = forwardRef(({ onDone }, if (prevState.experiments?.[id] === enabled) { return prevState } + + // console.log("setExperimentEnabled: setChangeDetected -> true") setChangeDetected(true) + return { ...prevState, experiments: { ...prevState.experiments, [id]: enabled }, } }) }, []) + const isSettingValid = !errorMessage + const handleSubmit = () => { if (isSettingValid) { vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly }) @@ -160,6 +158,7 @@ const SettingsView = forwardRef(({ onDone }, vscode.postMessage({ type: "updateExperimental", values: experiments }) vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch }) vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration }) + // console.log("handleSubmit: setChangeDetected -> false") setChangeDetected(false) } } @@ -176,13 +175,7 @@ const SettingsView = forwardRef(({ onDone }, [isChangeDetected], ) - useImperativeHandle( - ref, - () => ({ - checkUnsaveChanges, - }), - [checkUnsaveChanges], - ) + useImperativeHandle(ref, () => ({ checkUnsaveChanges }), [checkUnsaveChanges]) const onConfirmDialogResult = useCallback((confirm: boolean) => { if (confirm) { @@ -200,10 +193,7 @@ const SettingsView = forwardRef(({ onDone }, const newCommands = [...currentCommands, commandInput] setCachedStateField("allowedCommands", newCommands) setCommandInput("") - vscode.postMessage({ - type: "allowedCommands", - commands: newCommands, - }) + vscode.postMessage({ type: "allowedCommands", commands: newCommands }) } } diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx new file mode 100644 index 00000000000..efaa90dc39a --- /dev/null +++ b/webview-ui/src/components/settings/ThinkingBudget.tsx @@ -0,0 +1,29 @@ +import { Slider } from "@/components/ui" + +import { ApiConfiguration, ModelInfo, THINKING_BUDGET } from "../../../../src/shared/api" + +interface ThinkingBudgetProps { + apiConfiguration: ApiConfiguration + setApiConfigurationField: (field: K, value: ApiConfiguration[K]) => void + modelInfo?: ModelInfo +} + +export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, modelInfo }: ThinkingBudgetProps) => { + const budget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default + + return modelInfo && modelInfo.thinking ? ( +
+
Thinking Budget
+
+ setApiConfigurationField("anthropicThinking", value[0])} + /> +
{budget}
+
+
+ ) : null +} diff --git a/webview-ui/src/components/ui/alert-dialog.tsx b/webview-ui/src/components/ui/alert-dialog.tsx index 7530cae54d6..82a25bf8f70 100644 --- a/webview-ui/src/components/ui/alert-dialog.tsx +++ b/webview-ui/src/components/ui/alert-dialog.tsx @@ -4,94 +4,97 @@ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" -const AlertDialog = AlertDialogPrimitive.Root - -const AlertDialogTrigger = AlertDialogPrimitive.Trigger +function AlertDialog({ ...props }: React.ComponentProps) { + return +} -const AlertDialogPortal = AlertDialogPrimitive.Portal +function AlertDialogTrigger({ ...props }: React.ComponentProps) { + return +} -const AlertDialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName +function AlertDialogPortal({ ...props }: React.ComponentProps) { + return +} -const AlertDialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - ) { + return ( + - -)) -AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + ) +} -const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( -
-) -AlertDialogHeader.displayName = "AlertDialogHeader" +function AlertDialogContent({ className, ...props }: React.ComponentProps) { + return ( + + + + + ) +} -const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( -
-) -AlertDialogFooter.displayName = "AlertDialogFooter" +function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} -const AlertDialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName +function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ className, ...props }: React.ComponentProps) { + return ( + + ) +} -const AlertDialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} -const AlertDialogAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName +function AlertDialogAction({ className, ...props }: React.ComponentProps) { + return +} -const AlertDialogCancel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName +function AlertDialogCancel({ className, ...props }: React.ComponentProps) { + return +} export { AlertDialog, diff --git a/webview-ui/src/components/ui/dialog.tsx b/webview-ui/src/components/ui/dialog.tsx index 11d5e2d3b0c..ed3160f692a 100644 --- a/webview-ui/src/components/ui/dialog.tsx +++ b/webview-ui/src/components/ui/dialog.tsx @@ -1,96 +1,108 @@ -"use client" - import * as React from "react" import * as DialogPrimitive from "@radix-ui/react-dialog" -import { Cross2Icon } from "@radix-ui/react-icons" +import { XIcon } from "lucide-react" import { cn } from "@/lib/utils" -const Dialog = DialogPrimitive.Root - -const DialogTrigger = DialogPrimitive.Trigger +function Dialog({ ...props }: React.ComponentProps) { + return +} -const DialogPortal = DialogPrimitive.Portal +function DialogTrigger({ ...props }: React.ComponentProps) { + return +} -const DialogClose = DialogPrimitive.Close +function DialogPortal({ ...props }: React.ComponentProps) { + return +} -const DialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName +function DialogClose({ ...props }: React.ComponentProps) { + return +} -const DialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - ) { + return ( + - {children} - - - Close - - - -)) -DialogContent.displayName = DialogPrimitive.Content.displayName + {...props} + /> + ) +} -const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( -
-) -DialogHeader.displayName = "DialogHeader" +function DialogContent({ className, children, ...props }: React.ComponentProps) { + return ( + + + + {children} + + + Close + + + + ) +} -const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( -
-) -DialogFooter.displayName = "DialogFooter" +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} -const DialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogTitle.displayName = DialogPrimitive.Title.displayName +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} -const DialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogDescription.displayName = DialogPrimitive.Description.displayName +function DialogTitle({ className, ...props }: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ className, ...props }: React.ComponentProps) { + return ( + + ) +} export { Dialog, - DialogPortal, - DialogOverlay, - DialogTrigger, DialogClose, DialogContent, - DialogHeader, + DialogDescription, DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, DialogTitle, - DialogDescription, + DialogTrigger, } diff --git a/webview-ui/src/components/welcome/WelcomeView.tsx b/webview-ui/src/components/welcome/WelcomeView.tsx index 5d880efc0b9..ae674c895f4 100644 --- a/webview-ui/src/components/welcome/WelcomeView.tsx +++ b/webview-ui/src/components/welcome/WelcomeView.tsx @@ -12,16 +12,14 @@ const WelcomeView = () => { const handleSubmit = useCallback(() => { const error = validateApiConfiguration(apiConfiguration) + if (error) { setErrorMessage(error) return } + setErrorMessage(undefined) - vscode.postMessage({ - type: "upsertApiConfiguration", - text: currentApiConfigName, - apiConfiguration, - }) + vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration }) }, [apiConfiguration, currentApiConfigName]) return ( diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 97c702637c4..82af23ab497 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -1,74 +1,83 @@ -import { ApiConfiguration } from "../../../src/shared/api" -import { ModelInfo } from "../../../src/shared/api" +import { ApiConfiguration, ModelInfo } from "../../../src/shared/api" + export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined { - if (apiConfiguration) { - switch (apiConfiguration.apiProvider) { - case "anthropic": - if (!apiConfiguration.apiKey) { - return "You must provide a valid API key or choose a different provider." - } - break - case "glama": - if (!apiConfiguration.glamaApiKey) { - return "You must provide a valid API key or choose a different provider." - } - break - case "bedrock": - if (!apiConfiguration.awsRegion) { - return "You must choose a region to use with AWS Bedrock." - } - break - case "openrouter": - if (!apiConfiguration.openRouterApiKey) { - return "You must provide a valid API key or choose a different provider." - } - break - case "vertex": - if (!apiConfiguration.vertexProjectId || !apiConfiguration.vertexRegion) { - return "You must provide a valid Google Cloud Project ID and Region." - } - break - case "gemini": - if (!apiConfiguration.geminiApiKey) { - return "You must provide a valid API key or choose a different provider." - } - break - case "openai-native": - if (!apiConfiguration.openAiNativeApiKey) { - return "You must provide a valid API key or choose a different provider." - } - break - case "mistral": - if (!apiConfiguration.mistralApiKey) { - return "You must provide a valid API key or choose a different provider." - } - break - case "openai": - if ( - !apiConfiguration.openAiBaseUrl || - !apiConfiguration.openAiApiKey || - !apiConfiguration.openAiModelId - ) { - return "You must provide a valid base URL, API key, and model ID." - } - break - case "ollama": - if (!apiConfiguration.ollamaModelId) { - return "You must provide a valid model ID." - } - break - case "lmstudio": - if (!apiConfiguration.lmStudioModelId) { - return "You must provide a valid model ID." - } - break - case "vscode-lm": - if (!apiConfiguration.vsCodeLmModelSelector) { - return "You must provide a valid model selector." - } - break - } + if (!apiConfiguration) { + return undefined + } + + switch (apiConfiguration.apiProvider) { + case "openrouter": + if (!apiConfiguration.openRouterApiKey) { + return "You must provide a valid API key." + } + break + case "glama": + if (!apiConfiguration.glamaApiKey) { + return "You must provide a valid API key." + } + break + case "unbound": + if (!apiConfiguration.unboundApiKey) { + return "You must provide a valid API key." + } + break + case "requesty": + if (!apiConfiguration.requestyApiKey) { + return "You must provide a valid API key." + } + break + case "anthropic": + if (!apiConfiguration.apiKey) { + return "You must provide a valid API key." + } + break + case "bedrock": + if (!apiConfiguration.awsRegion) { + return "You must choose a region to use with AWS Bedrock." + } + break + case "vertex": + if (!apiConfiguration.vertexProjectId || !apiConfiguration.vertexRegion) { + return "You must provide a valid Google Cloud Project ID and Region." + } + break + case "gemini": + if (!apiConfiguration.geminiApiKey) { + return "You must provide a valid API key." + } + break + case "openai-native": + if (!apiConfiguration.openAiNativeApiKey) { + return "You must provide a valid API key." + } + break + case "mistral": + if (!apiConfiguration.mistralApiKey) { + return "You must provide a valid API key." + } + break + case "openai": + if (!apiConfiguration.openAiBaseUrl || !apiConfiguration.openAiApiKey || !apiConfiguration.openAiModelId) { + return "You must provide a valid base URL, API key, and model ID." + } + break + case "ollama": + if (!apiConfiguration.ollamaModelId) { + return "You must provide a valid model ID." + } + break + case "lmstudio": + if (!apiConfiguration.lmStudioModelId) { + return "You must provide a valid model ID." + } + break + case "vscode-lm": + if (!apiConfiguration.vsCodeLmModelSelector) { + return "You must provide a valid model selector." + } + break } + return undefined } @@ -77,40 +86,81 @@ export function validateModelId( glamaModels?: Record, openRouterModels?: Record, unboundModels?: Record, + requestyModels?: Record, ): string | undefined { - if (apiConfiguration) { - switch (apiConfiguration.apiProvider) { - case "glama": - const glamaModelId = apiConfiguration.glamaModelId - if (!glamaModelId) { - return "You must provide a model ID." - } - if (glamaModels && !Object.keys(glamaModels).includes(glamaModelId)) { - // even if the model list endpoint failed, extensionstatecontext will always have the default model info - return "The model ID you provided is not available. Please choose a different model." - } - break - case "openrouter": - const modelId = apiConfiguration.openRouterModelId - if (!modelId) { - return "You must provide a model ID." - } - if (openRouterModels && !Object.keys(openRouterModels).includes(modelId)) { - // even if the model list endpoint failed, extensionstatecontext will always have the default model info - return "The model ID you provided is not available. Please choose a different model." - } - break - case "unbound": - const unboundModelId = apiConfiguration.unboundModelId - if (!unboundModelId) { - return "You must provide a model ID." - } - if (unboundModels && !Object.keys(unboundModels).includes(unboundModelId)) { - // even if the model list endpoint failed, extensionstatecontext will always have the default model info - return "The model ID you provided is not available. Please choose a different model." - } - break - } + if (!apiConfiguration) { + return undefined + } + + switch (apiConfiguration.apiProvider) { + case "openrouter": + const modelId = apiConfiguration.openRouterModelId + + if (!modelId) { + return "You must provide a model ID." + } + + if ( + openRouterModels && + Object.keys(openRouterModels).length > 1 && + !Object.keys(openRouterModels).includes(modelId) + ) { + return `The model ID (${modelId}) you provided is not available. Please choose a different model.` + } + + break + + case "glama": + const glamaModelId = apiConfiguration.glamaModelId + + if (!glamaModelId) { + return "You must provide a model ID." + } + + if ( + glamaModels && + Object.keys(glamaModels).length > 1 && + !Object.keys(glamaModels).includes(glamaModelId) + ) { + return `The model ID (${glamaModelId}) you provided is not available. Please choose a different model.` + } + + break + + case "unbound": + const unboundModelId = apiConfiguration.unboundModelId + + if (!unboundModelId) { + return "You must provide a model ID." + } + + if ( + unboundModels && + Object.keys(unboundModels).length > 1 && + !Object.keys(unboundModels).includes(unboundModelId) + ) { + return `The model ID (${unboundModelId}) you provided is not available. Please choose a different model.` + } + + break + + case "requesty": + const requestyModelId = apiConfiguration.requestyModelId + + if (!requestyModelId) { + return "You must provide a model ID." + } + + if ( + requestyModels && + Object.keys(requestyModels).length > 1 && + !Object.keys(requestyModels).includes(requestyModelId) + ) { + return `The model ID (${requestyModelId}) you provided is not available. Please choose a different model.` + } + + break } + return undefined }