diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index f4a50cb7f0..73bec501c5 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -72,7 +72,7 @@ const ApiOptions = ({ setErrorMessage, }: ApiOptionsProps) => { const { t } = useAppTranslation() - const { organizationAllowList } = useExtensionState() + const { organizationAllowList, areProviderModelsLoading } = useExtensionState() const refetchRouterModels = useCallback(() => { vscode.postMessage({ @@ -130,11 +130,16 @@ const ApiOptions = ({ } }, [selectedModelId, setApiConfigurationField]) + // Validation logic using the global loading state useEffect(() => { - const apiValidationResult = validateApiConfiguration(apiConfiguration, organizationAllowList) - - setErrorMessage(apiValidationResult) - }, [apiConfiguration, organizationAllowList, setErrorMessage]) + if (!areProviderModelsLoading) { + // Only validate if provider models are not currently loading + const apiValidationResult = validateApiConfiguration(apiConfiguration, organizationAllowList) + setErrorMessage(apiValidationResult) + } else { + setErrorMessage(undefined) + } + }, [apiConfiguration, organizationAllowList, setErrorMessage, areProviderModelsLoading]) const selectedProviderModels = useMemo(() => { const models = MODELS_BY_PROVIDER[selectedProvider] diff --git a/webview-ui/src/components/settings/providers/Glama.tsx b/webview-ui/src/components/settings/providers/Glama.tsx index fde9f2b2df..62c942a156 100644 --- a/webview-ui/src/components/settings/providers/Glama.tsx +++ b/webview-ui/src/components/settings/providers/Glama.tsx @@ -1,4 +1,4 @@ -import { useCallback } from "react" +import { useCallback, useMemo } from "react" import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import type { ProviderSettings, OrganizationAllowList } from "@roo-code/types" @@ -22,7 +22,19 @@ type GlamaProps = { export const Glama = ({ apiConfiguration, setApiConfigurationField, uriScheme, organizationAllowList }: GlamaProps) => { const { t } = useAppTranslation() - const { models: glamaModelsData, isLoading: isLoadingModels, error: modelsError } = useProviderModels("glama") + const providerModelsOptions = useMemo( + () => ({ + flushCacheFirst: true, + }), + [], + ) + + const { + models: glamaModelsData, + isLoading: isLoadingModels, + error: modelsError, + refetch: refetchGlamaModels, + } = useProviderModels("glama", providerModelsOptions) const handleInputChange = useCallback( ( @@ -70,6 +82,7 @@ export const Glama = ({ apiConfiguration, setApiConfigurationField, uriScheme, o serviceName="Glama" serviceUrl="https://glama.ai/models" organizationAllowList={organizationAllowList} + onOpenRefetch={refetchGlamaModels} /> ) diff --git a/webview-ui/src/components/settings/providers/Ollama.tsx b/webview-ui/src/components/settings/providers/Ollama.tsx index c69452180f..9142848f79 100644 --- a/webview-ui/src/components/settings/providers/Ollama.tsx +++ b/webview-ui/src/components/settings/providers/Ollama.tsx @@ -1,4 +1,4 @@ -import { useCallback } from "react" +import { useCallback, useMemo } from "react" import { VSCodeTextField, VSCodeRadioGroup, VSCodeRadio } from "@vscode/webview-ui-toolkit/react" import type { ProviderSettings } from "@roo-code/types" @@ -15,7 +15,20 @@ type OllamaProps = { export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaProps) => { const { t } = useAppTranslation() - const { models: ollamaModelsData, isLoading: isLoadingModels, error: modelsError } = useProviderModels("ollama") + const providerModelsOptions = useMemo( + () => ({ + flushCacheFirst: true, + baseUrl: apiConfiguration?.ollamaBaseUrl, + }), + [apiConfiguration?.ollamaBaseUrl], + ) + + const { + models: ollamaModelsData, + isLoading: isLoadingModels, + error: modelsError, + // refetch is not used directly by this component for now + } = useProviderModels("ollama", providerModelsOptions) const handleInputChange = useCallback( ( diff --git a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx index 0d2cfaa2fe..cab4154bd9 100644 --- a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect } from "react" +import { useState, useCallback, useEffect, useMemo } from "react" import { Checkbox } from "vscrui" import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" @@ -32,11 +32,24 @@ export const OpenAICompatible = ({ const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion) const [openAiLegacyFormatSelected, setOpenAiLegacyFormatSelected] = useState(!!apiConfiguration?.openAiLegacyFormat) + const providerModelsOptions = useMemo(() => { + if (!apiConfiguration?.openAiBaseUrl) { + return undefined + } + return { + flushCacheFirst: true, + baseUrl: apiConfiguration.openAiBaseUrl, + apiKey: apiConfiguration?.openAiApiKey, + openAiHeaders: apiConfiguration?.openAiHeaders, + } + }, [apiConfiguration?.openAiBaseUrl, apiConfiguration?.openAiApiKey, apiConfiguration?.openAiHeaders]) + const { models: openAiCompatibleModels, isLoading: isLoadingOpenAiCompatibleModels, error: openAiCompatibleModelsError, - } = useProviderModels("openai-compatible") + refetch: refetchOpenAiCompatibleModels, + } = useProviderModels("openai-compatible", providerModelsOptions) const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => { const headers = apiConfiguration?.openAiHeaders || {} @@ -131,6 +144,7 @@ export const OpenAICompatible = ({ serviceName="OpenAI" serviceUrl="https://platform.openai.com" organizationAllowList={organizationAllowList} + onOpenRefetch={refetchOpenAiCompatibleModels} /> { const { t } = useAppTranslation() - const { models: requestyModelsData, isLoading: isLoadingModels, error: modelsError } = useProviderModels("requesty") + const providerModelsOptions = useMemo( + () => ({ + flushCacheFirst: true, + requestyApiKey: apiConfiguration?.requestyApiKey, + }), + [apiConfiguration?.requestyApiKey], + ) + + const { + models: requestyModelsData, + isLoading: isLoadingModels, + error: modelsError, + refetch: refetchRequestyModels, + } = useProviderModels("requesty", providerModelsOptions) const handleInputChange = useCallback( ( @@ -83,6 +96,7 @@ export const Requesty = ({ apiConfiguration, setApiConfigurationField, organizat serviceName="Requesty" serviceUrl="https://requesty.ai" organizationAllowList={organizationAllowList} + onOpenRefetch={refetchRequestyModels} /> ) diff --git a/webview-ui/src/components/settings/providers/Unbound.tsx b/webview-ui/src/components/settings/providers/Unbound.tsx index 4256ceac66..8c0f3e8f07 100644 --- a/webview-ui/src/components/settings/providers/Unbound.tsx +++ b/webview-ui/src/components/settings/providers/Unbound.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState, useEffect, useRef } from "react" +import { useCallback, useState, useEffect, useRef, useMemo } from "react" import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import type { ProviderSettings, OrganizationAllowList } from "@roo-code/types" @@ -20,7 +20,20 @@ type UnboundProps = { export const Unbound = ({ apiConfiguration, setApiConfigurationField, organizationAllowList }: UnboundProps) => { const { t } = useAppTranslation() - const { models: unboundModelsData, isLoading: isLoadingModels, error: modelsError } = useProviderModels("unbound") + const providerModelsOptions = useMemo( + () => ({ + flushCacheFirst: true, + unboundApiKey: apiConfiguration?.unboundApiKey, + }), + [apiConfiguration?.unboundApiKey], + ) + + const { + models: unboundModelsData, + isLoading: isLoadingModels, + error: modelsError, + refetch: refetchUnboundModels, + } = useProviderModels("unbound", providerModelsOptions) const [isInvalidKeyFeedback, setIsInvalidKeyFeedback] = useState(false) const invalidKeyTimerRef = useRef() @@ -101,6 +114,7 @@ export const Unbound = ({ apiConfiguration, setApiConfigurationField, organizati serviceUrl="https://api.getunbound.ai/models" setApiConfigurationField={setApiConfigurationField} organizationAllowList={organizationAllowList} + onOpenRefetch={refetchUnboundModels} /> ) diff --git a/webview-ui/src/components/ui/hooks/useProviderModels.ts b/webview-ui/src/components/ui/hooks/useProviderModels.ts index c78e5a9497..31d933eeda 100644 --- a/webview-ui/src/components/ui/hooks/useProviderModels.ts +++ b/webview-ui/src/components/ui/hooks/useProviderModels.ts @@ -5,19 +5,20 @@ import { RouterName, ModelRecord } from "@roo/api" import { ExtensionMessage } from "@roo/ExtensionMessage" import { vscode } from "@src/utils/vscode" import { useDebounceEffect } from "@src/utils/useDebounceEffect" +import { useExtensionState } from "@src/context/ExtensionStateContext" // --- START: Type definitions for provider-specific params --- // Inspired by GetModelsOptions from src/shared/api.ts // These are the *additional* params a provider might need, sent from the UI. export type ProviderSpecificParamsMap = { - openrouter: Record - glama: Record + openrouter: object + glama: object requesty: { requestyApiKey?: string } unbound: { unboundApiKey?: string } litellm: { litellmApiKey?: string; litellmBaseUrl?: string } ollama: { baseUrl?: string } lmstudio: { baseUrl?: string } - vscodelm: Record + vscodelm: object "openai-compatible": { baseUrl: string apiKey?: string @@ -103,6 +104,7 @@ export const useProviderModels =

( options?: UseProviderModelsOptions

, ): UseProviderModelsResult => { const queryClient = useQueryClient() + const { setAreProviderModelsLoading } = useExtensionState() // Track if we're currently debouncing const debouncingRef = useRef(false) @@ -171,6 +173,13 @@ export const useProviderModels =

( // Clear error when in loading state const error = isLoading ? undefined : queryError?.message + // Update global loading state + useEffect(() => { + if (setAreProviderModelsLoading) { + setAreProviderModelsLoading(isLoading) + } + }, [isLoading, setAreProviderModelsLoading]) + return { models: data, isLoading, diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 7952ee7721..57d98e256a 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -114,6 +114,8 @@ export interface ExtensionStateContextType extends ExtensionState { setAutoCondenseContext: (value: boolean) => void autoCondenseContextPercent: number setAutoCondenseContextPercent: (value: number) => void + areProviderModelsLoading?: boolean + setAreProviderModelsLoading?: (isLoading: boolean) => void } export const ExtensionStateContext = createContext(undefined) @@ -146,7 +148,9 @@ export const mergeExtensionState = (prevState: ExtensionState, newState: Extensi } export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [state, setState] = useState({ + const [state, setState] = useState< + ExtensionState & { organizationAllowList?: OrganizationAllowList; areProviderModelsLoading?: boolean } + >({ version: "", clineMessages: [], taskHistory: [], @@ -206,6 +210,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode codebaseIndexEmbedderModelId: "", }, codebaseIndexModels: { ollama: {}, openai: {} }, + areProviderModelsLoading: false, }) const [didHydrateState, setDidHydrateState] = useState(false) @@ -391,6 +396,9 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setCondensingApiConfigId: (value) => setState((prevState) => ({ ...prevState, condensingApiConfigId: value })), setCustomCondensingPrompt: (value) => setState((prevState) => ({ ...prevState, customCondensingPrompt: value })), + areProviderModelsLoading: state.areProviderModelsLoading, + setAreProviderModelsLoading: (isLoading: boolean) => + setState((prevState) => ({ ...prevState, areProviderModelsLoading: isLoading })), } return {children}