Files
tkmind_go/ui/desktop/src/components/ModelAndProviderContext.tsx
T

239 lines
7.8 KiB
TypeScript

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<boolean>;
getCurrentModelAndProvider: () => Promise<{ model: string; provider: string }>;
getFallbackModelAndProvider: () => Promise<{ model: string; provider: string }>;
getCurrentModelAndProviderForDisplay: () => Promise<{ model: string; provider: string }>;
getCurrentModelDisplayName: () => Promise<string>;
getCurrentProviderDisplayName: () => Promise<string>; // Gets provider display name from subtext
refreshCurrentModelAndProvider: () => Promise<void>;
}
interface ModelAndProviderProviderProps {
children: React.ReactNode;
}
const ModelAndProviderContext = createContext<ModelAndProviderContextType | undefined>(undefined);
export { i18n as modelAndProviderMessages };
export const ModelAndProviderProvider: React.FC<ModelAndProviderProviderProps> = ({ children }) => {
const [currentModel, setCurrentModel] = useState<string | null>(null);
const [currentProvider, setCurrentProvider] = useState<string | null>(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 (
<ModelAndProviderContext.Provider value={contextValue}>
{children}
</ModelAndProviderContext.Provider>
);
};
export const useModelAndProvider = () => {
const context = useContext(ModelAndProviderContext);
if (context === undefined) {
throw new Error('useModelAndProvider must be used within a ModelAndProviderProvider');
}
return context;
};