import { useEffect, useState, useCallback } from 'react'; import { Bot, ExternalLink } from 'lucide-react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '../../../ui/dialog'; import { Button } from '../../../ui/button'; import { QUICKSTART_GUIDE_URL } from '../../providers/modal/constants'; import { Input } from '../../../ui/input'; import { Select } from '../../../ui/Select'; import { useConfig } from '../../../ConfigContext'; import { useModelAndProvider } from '../../../ModelAndProviderContext'; import type { View } from '../../../../utils/navigationUtils'; import Model, { getProviderMetadata, fetchModelsForProviders } from '../modelInterface'; import { getPredefinedModelsFromEnv, shouldShowPredefinedModels } from '../predefinedModelsUtils'; import { ProviderType } from '../../../../api'; import { trackModelChanged } from '../../../../utils/analytics'; const PREFERRED_MODEL_PATTERNS = [ /claude-sonnet-4/i, /claude-4/i, /gpt-4o(?!-mini)/i, /claude-3-5-sonnet/i, /claude-3\.5-sonnet/i, /gpt-4-turbo/i, /gpt-4(?!-|o)/i, /claude-3-opus/i, /claude-3-sonnet/i, /gemini-pro/i, /llama-3/i, /gpt-4o-mini/i, /claude-3-haiku/i, /gemini/i, ]; function findPreferredModel( models: { value: string; label: string; provider: string }[] ): string | null { if (models.length === 0) return null; const validModels = models.filter( (m) => m.value !== 'custom' && m.value !== '__loading__' && !m.value.startsWith('__') ); if (validModels.length === 0) return null; for (const pattern of PREFERRED_MODEL_PATTERNS) { const match = validModels.find((m) => pattern.test(m.value)); if (match) { return match.value; } } return validModels[0].value; } type SwitchModelModalProps = { sessionId: string | null; onClose: () => void; setView: (view: View) => void; onModelSelected?: (model: string) => void; initialProvider?: string | null; titleOverride?: string; }; export const SwitchModelModal = ({ sessionId, onClose, setView, onModelSelected, initialProvider, titleOverride, }: SwitchModelModalProps) => { const { getProviders, getProviderModels, read } = useConfig(); const { changeModel, currentModel, currentProvider } = useModelAndProvider(); const [providerOptions, setProviderOptions] = useState<{ value: string; label: string }[]>([]); type ModelOption = { value: string; label: string; provider: string; isDisabled?: boolean }; const [modelOptions, setModelOptions] = useState<{ options: ModelOption[] }[]>([]); const [provider, setProvider] = useState( initialProvider || currentProvider || null ); const [model, setModel] = useState(currentModel || ''); const [isCustomModel, setIsCustomModel] = useState(false); const [validationErrors, setValidationErrors] = useState({ provider: '', model: '', }); const [isValid, setIsValid] = useState(true); const [attemptedSubmit, setAttemptedSubmit] = useState(false); const [usePredefinedModels] = useState(shouldShowPredefinedModels()); const [selectedPredefinedModel, setSelectedPredefinedModel] = useState(null); const [predefinedModels, setPredefinedModels] = useState([]); const [loadingModels, setLoadingModels] = useState(false); const [userClearedModel, setUserClearedModel] = useState(false); // Validate form data const validateForm = useCallback(() => { const errors = { provider: '', model: '', }; let formIsValid = true; if (usePredefinedModels) { if (!selectedPredefinedModel) { errors.model = 'Please select a model'; formIsValid = false; } } else { if (!provider) { errors.provider = 'Please select a provider'; formIsValid = false; } if (!model) { errors.model = 'Please select or enter a model'; formIsValid = false; } } setValidationErrors(errors); setIsValid(formIsValid); return formIsValid; }, [model, provider, usePredefinedModels, selectedPredefinedModel]); const handleClose = () => { onClose(); }; const handleSubmit = async () => { setAttemptedSubmit(true); const isFormValid = validateForm(); if (isFormValid) { let modelObj: Model; if (usePredefinedModels && selectedPredefinedModel) { modelObj = selectedPredefinedModel; } else { const providerMetaData = await getProviderMetadata(provider || '', getProviders); const providerDisplayName = providerMetaData.display_name; modelObj = { name: model, provider: provider, subtext: providerDisplayName } as Model; } await changeModel(sessionId, modelObj); trackModelChanged(modelObj.provider || '', modelObj.name); if (onModelSelected) { onModelSelected(modelObj.name); } onClose(); } }; // Re-validate when inputs change and after attempted submission useEffect(() => { if (attemptedSubmit) { validateForm(); } }, [attemptedSubmit, validateForm]); useEffect(() => { // Load predefined models if enabled if (usePredefinedModels) { const models = getPredefinedModelsFromEnv(); setPredefinedModels(models); // Initialize selected predefined model with current model (async () => { try { const currentModelName = (await read('GOOSE_MODEL', false)) as string; const matchingModel = models.find((model) => model.name === currentModelName); if (matchingModel) { setSelectedPredefinedModel(matchingModel); } } catch (error) { console.error('Failed to get current model for selection:', error); } })(); } // Load providers for manual model selection (async () => { try { const providersResponse = await getProviders(false); const activeProviders = providersResponse.filter((provider) => provider.is_configured); // Create provider options and add "Use other provider" option setProviderOptions([ ...activeProviders.map(({ metadata, name }) => ({ value: name, label: metadata.display_name, })), { value: 'configure_providers', label: 'Use other provider', }, ]); setLoadingModels(true); // Fetching models for all providers (always recommended) const results = await fetchModelsForProviders(activeProviders, getProviderModels); // Process results and build grouped options const groupedOptions: { options: { value: string; label: string; provider: string; providerType: ProviderType }[]; }[] = []; const errors: string[] = []; results.forEach(({ provider: p, models, error }) => { if (error) { errors.push(error); // Fallback to metadata known_models on error if (p.metadata.known_models && p.metadata.known_models.length > 0) { groupedOptions.push({ options: p.metadata.known_models.map(({ name }) => ({ value: name, label: name, providerType: p.provider_type, provider: p.name, })), }); } } else if (models && models.length > 0) { groupedOptions.push({ options: models.map((m) => ({ value: m, label: m, provider: p.name, providerType: p.provider_type, })), }); } }); // Log errors if any providers failed (don't show to user) if (errors.length > 0) { console.error('Provider model fetch errors:', errors); } // Add the "Custom model" option to each provider group groupedOptions.forEach((group) => { const option = group.options[0]; const providerName = option?.provider; if (providerName && option?.providerType !== 'Custom') { group.options.push({ value: 'custom', label: 'Use custom model', provider: providerName, providerType: option?.providerType, }); } }); setModelOptions(groupedOptions); setOriginalModelOptions(groupedOptions); } catch (error: unknown) { console.error('Failed to query providers:', error); } finally { setLoadingModels(false); } })(); }, [getProviders, getProviderModels, usePredefinedModels, read]); const filteredModelOptions = provider ? modelOptions.filter((group) => group.options[0]?.provider === provider) : []; useEffect(() => { // Don't auto-select if user explicitly cleared the model if (!provider || loadingModels || model || isCustomModel || userClearedModel) return; const providerModels = modelOptions .filter((group) => group.options[0]?.provider === provider) .flatMap((group) => group.options); if (providerModels.length > 0) { const preferredModel = findPreferredModel(providerModels); if (preferredModel) { setModel(preferredModel); } } }, [provider, modelOptions, loadingModels, model, isCustomModel, userClearedModel]); // Handle model selection change const handleModelChange = (newValue: unknown) => { const selectedOption = newValue as { value: string; label: string; provider: string } | null; if (selectedOption?.value === 'custom') { setIsCustomModel(true); setModel(''); setUserClearedModel(false); } else if (selectedOption === null) { // User cleared the selection setIsCustomModel(false); setModel(''); setUserClearedModel(true); } else { setIsCustomModel(false); setModel(selectedOption?.value || ''); setUserClearedModel(false); } }; // Store the original model options in state, initialized from modelOptions const [originalModelOptions, setOriginalModelOptions] = useState<{ options: { value: string; label: string; provider: string }[] }[]>(modelOptions); const handleInputChange = (inputValue: string) => { if (!provider) return; const trimmedInput = inputValue.trim(); if (trimmedInput === '') { // Reset to original model options when input is cleared setModelOptions([...originalModelOptions]); // Create new array to ensure state update return; } // Filter through the original model options to find matches const matchingOptions = originalModelOptions .map((group) => ({ options: group.options.filter( (option) => option.value.toLowerCase().includes(trimmedInput.toLowerCase()) && option.value !== 'custom' // Exclude the "Use custom model" option from search ), })) .filter((group) => group.options.length > 0); if (matchingOptions.length > 0) { // If we found matches in the existing options, show those setModelOptions(matchingOptions); } else { // If no matches, show the "Use: " option const customOption = [ { options: [ { value: trimmedInput, label: `Use: "${trimmedInput}"`, provider: provider, }, ], }, ]; setModelOptions(customOption); } }; return ( {titleOverride || 'Switch models'} Select a provider and model to use for your conversations.
{usePredefinedModels ? (
{predefinedModels.map((model) => (
setSelectedPredefinedModel(model)} >
{model.alias || model.name} {model.alias?.includes('recommended') && ( Recommended )}
{model.subtext} {model.provider}
setSelectedPredefinedModel(model)} className="peer sr-only" />
))}
{attemptedSubmit && validationErrors.model && (
{validationErrors.model}
)}
) : ( /* Manual Provider/Model Selection */
0 ? filteredModelOptions : [] } onChange={handleModelChange} onInputChange={handleInputChange} value={model ? { value: model, label: model } : null} placeholder={ loadingModels ? 'Loading models…' : 'Select a model, type to search' } isClearable isDisabled={loadingModels} /> {attemptedSubmit && validationErrors.model && (
{validationErrors.model}
)}
) : (
setModel(event.target.value)} value={model} /> {attemptedSubmit && validationErrors.model && (
{validationErrors.model}
)}
)} )}
)}
Quick start guide
); };