import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react'; import { toastError, toastSuccess } from '../toasts'; import Model, { getProviderMetadata } from './settings/models/modelInterface'; import { ProviderMetadata } from '../api'; import { acpReadDefaults, acpSaveDefaults, acpSetSessionProviderModel } from '../acp/providers'; import { errorMessage } from '../utils/conversionUtils'; import { getModelDisplayName, getProviderDisplayName, } from './settings/models/predefinedModelsUtils'; import { defineMessages, useIntl } from '../i18n'; const i18n = defineMessages({ unknownProviderTitle: { id: 'modelAndProviderContext.unknownProviderTitle', defaultMessage: 'Provider name lookup', }, unknownProviderMsg: { id: 'modelAndProviderContext.unknownProviderMsg', defaultMessage: 'Unknown provider in config -- please inspect your config.yaml', }, modelChangedTitle: { id: 'modelAndProviderContext.modelChangedTitle', defaultMessage: 'Model changed', }, switchModelSuccess: { id: 'modelAndProviderContext.switchModelSuccess', defaultMessage: 'Successfully switched models -- using {model} from {provider}', }, modelChangeFailed: { id: 'modelAndProviderContext.modelChangeFailed', defaultMessage: '{provider}/{model} failed', }, selectModel: { id: 'modelAndProviderContext.selectModel', defaultMessage: 'Select Model', }, }); interface ModelAndProviderContextType { currentModel: string | null; currentProvider: string | null; changeModel: (sessionId: string | null, model: Model) => Promise; getCurrentModelAndProvider: () => Promise<{ model: string; provider: string }>; getFallbackModelAndProvider: () => Promise<{ model: string; provider: string }>; getCurrentModelAndProviderForDisplay: () => Promise<{ model: string; provider: string }>; getCurrentModelDisplayName: () => Promise; getCurrentProviderDisplayName: () => Promise; // Gets provider display name from subtext refreshCurrentModelAndProvider: () => Promise; } interface ModelAndProviderProviderProps { children: React.ReactNode; } const ModelAndProviderContext = createContext(undefined); export { i18n as modelAndProviderMessages }; export const ModelAndProviderProvider: React.FC = ({ children }) => { const [currentModel, setCurrentModel] = useState(null); const [currentProvider, setCurrentProvider] = useState(null); const intl = useIntl(); const changeModel = useCallback( async (sessionId: string | null, model: Model) => { const modelName = model.name; const providerName = model.provider; let phase = 'agent'; try { if (sessionId) { await acpSetSessionProviderModel(sessionId, providerName, modelName); } // Only update the global config default when there's no session // (i.e. changing from settings, not from within an existing chat) if (!sessionId) { phase = 'config'; await acpSaveDefaults(providerName, modelName); } if (!sessionId) { setCurrentProvider(providerName); setCurrentModel(modelName); } toastSuccess({ title: intl.formatMessage(i18n.modelChangedTitle), msg: intl.formatMessage(i18n.switchModelSuccess, { model: model.alias ?? modelName, provider: model.subtext ?? providerName, }), }); return true; } catch (error) { console.error(`Failed to change model at ${phase} step -- ${modelName} ${providerName}`); toastError({ title: intl.formatMessage(i18n.modelChangeFailed, { provider: providerName, model: modelName, }), msg: `${error}`, traceback: errorMessage(error), }); return false; } }, [intl] ); const getFallbackModelAndProvider = useCallback(async () => { const provider = window.appConfig.get('GOOSE_DEFAULT_PROVIDER') as string; const model = window.appConfig.get('GOOSE_DEFAULT_MODEL') as string; if (provider && model) { try { await acpSaveDefaults(provider, model); } catch (error) { console.error('[getFallbackModelAndProvider] Failed to write to config', error); } } return { model: model, provider: provider }; }, []); const getCurrentModelAndProvider = useCallback(async () => { let model: string | null; let provider: string | null; try { const defaults = await acpReadDefaults(); model = defaults.modelId; provider = defaults.providerId; } catch { console.error(`Failed to read default model or provider`); throw new Error('Failed to read default model or provider'); } if (!model || !provider) { return getFallbackModelAndProvider(); } return { model: model, provider: provider }; }, [getFallbackModelAndProvider]); const getCurrentModelAndProviderForDisplay = useCallback(async () => { const modelProvider = await getCurrentModelAndProvider(); const gooseModel = modelProvider.model; const gooseProvider = modelProvider.provider; // lookup display name let metadata: ProviderMetadata; try { metadata = await getProviderMetadata(String(gooseProvider)); } catch { return { model: gooseModel, provider: gooseProvider }; } const providerDisplayName = metadata.display_name; return { model: gooseModel, provider: providerDisplayName }; }, [getCurrentModelAndProvider]); const getCurrentModelDisplayName = useCallback(async () => { try { const { modelId } = await acpReadDefaults(); return getModelDisplayName(modelId ?? ''); } catch { return intl.formatMessage(i18n.selectModel); } }, [intl]); const getCurrentProviderDisplayName = useCallback(async () => { try { const { modelId } = await acpReadDefaults(); const providerDisplayName = getProviderDisplayName(modelId ?? ''); if (providerDisplayName) { return providerDisplayName; } // Fall back to regular provider display name lookup const { provider } = await getCurrentModelAndProviderForDisplay(); return provider; } catch { return ''; } }, [getCurrentModelAndProviderForDisplay]); const refreshCurrentModelAndProvider = useCallback(async () => { try { const { model, provider } = await getCurrentModelAndProvider(); setCurrentModel(model); setCurrentProvider(provider); } catch (_error) { console.error('Failed to refresh current model and provider:', _error); } }, [getCurrentModelAndProvider]); // Load initial model and provider on mount useEffect(() => { refreshCurrentModelAndProvider(); }, [refreshCurrentModelAndProvider]); const contextValue = useMemo( () => ({ currentModel, currentProvider, changeModel, getCurrentModelAndProvider, getFallbackModelAndProvider, getCurrentModelAndProviderForDisplay, getCurrentModelDisplayName, getCurrentProviderDisplayName, refreshCurrentModelAndProvider, }), [ currentModel, currentProvider, changeModel, getCurrentModelAndProvider, getFallbackModelAndProvider, getCurrentModelAndProviderForDisplay, getCurrentModelDisplayName, getCurrentProviderDisplayName, refreshCurrentModelAndProvider, ] ); return ( {children} ); }; export const useModelAndProvider = () => { const context = useContext(ModelAndProviderContext); if (context === undefined) { throw new Error('useModelAndProvider must be used within a ModelAndProviderProvider'); } return context; };