LibreChat/client/src/components/Chat/Menus/Endpoints/ModelSelectorContext.tsx
Danny Avila 90b8769ef3
🚀 feat: Use Model Specs + Specific Endpoints, Limit Providers for Agents (#6650)
* 🔧 refactor: Remove modelSpecs prop from ModelSelector and related components

* fix: Update submission.conversationId references in SSE hooks and data types as was incorrectly typed

* feat: Allow showing specific endpoints alongside model specs via `addedEndpoints` field

* feat: allowed agents providers via `agents.allowedProviders` field

* fix: bump dicebear/sharp dependencies to resolve CVE-2024-12905 and improve avatar gen logic

* fix: rename variable for clarity in loadDefaultInterface function

* fix: add keepAddedConvos option to newConversation calls for modular chat support

* fix: include model information in endpoint selection for improved context

* fix: update data-provider version to 0.7.78 and increment config version to 1.2.4
2025-04-01 03:50:32 -04:00

188 lines
5.7 KiB
TypeScript

import debounce from 'lodash/debounce';
import React, { createContext, useContext, useState, useMemo } from 'react';
import { isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import type * as t from 'librechat-data-provider';
import type { Endpoint, SelectedValues } from '~/common';
import { useAgentsMapContext, useAssistantsMapContext, useChatContext } from '~/Providers';
import { useEndpoints, useSelectorEffects, useKeyDialog } from '~/hooks';
import useSelectMention from '~/hooks/Input/useSelectMention';
import { useGetEndpointsQuery } from '~/data-provider';
import { filterItems } from './utils';
type ModelSelectorContextType = {
// State
searchValue: string;
selectedValues: SelectedValues;
endpointSearchValues: Record<string, string>;
searchResults: (t.TModelSpec | Endpoint)[] | null;
// LibreChat
modelSpecs: t.TModelSpec[];
mappedEndpoints: Endpoint[];
agentsMap: t.TAgentsMap | undefined;
assistantsMap: t.TAssistantsMap | undefined;
endpointsConfig: t.TEndpointsConfig;
// Functions
endpointRequiresUserKey: (endpoint: string) => boolean;
setSelectedValues: React.Dispatch<React.SetStateAction<SelectedValues>>;
setSearchValue: (value: string) => void;
setEndpointSearchValue: (endpoint: string, value: string) => void;
handleSelectSpec: (spec: t.TModelSpec) => void;
handleSelectEndpoint: (endpoint: Endpoint) => void;
handleSelectModel: (endpoint: Endpoint, model: string) => void;
} & ReturnType<typeof useKeyDialog>;
const ModelSelectorContext = createContext<ModelSelectorContextType | undefined>(undefined);
export function useModelSelectorContext() {
const context = useContext(ModelSelectorContext);
if (context === undefined) {
throw new Error('useModelSelectorContext must be used within a ModelSelectorProvider');
}
return context;
}
interface ModelSelectorProviderProps {
children: React.ReactNode;
startupConfig: t.TStartupConfig | undefined;
}
export function ModelSelectorProvider({ children, startupConfig }: ModelSelectorProviderProps) {
const agentsMap = useAgentsMapContext();
const assistantsMap = useAssistantsMapContext();
const { data: endpointsConfig } = useGetEndpointsQuery();
const { conversation, newConversation } = useChatContext();
const modelSpecs = useMemo(() => startupConfig?.modelSpecs?.list ?? [], [startupConfig]);
const { mappedEndpoints, endpointRequiresUserKey } = useEndpoints({
agentsMap,
assistantsMap,
startupConfig,
endpointsConfig,
});
const { onSelectEndpoint, onSelectSpec } = useSelectMention({
// presets,
modelSpecs,
assistantsMap,
endpointsConfig,
newConversation,
returnHandlers: true,
});
// State
const [selectedValues, setSelectedValues] = useState<SelectedValues>({
endpoint: conversation?.endpoint || '',
model: conversation?.model || '',
modelSpec: conversation?.spec || '',
});
useSelectorEffects({
agentsMap,
conversation,
assistantsMap,
setSelectedValues,
});
const [searchValue, setSearchValueState] = useState('');
const [endpointSearchValues, setEndpointSearchValues] = useState<Record<string, string>>({});
const keyProps = useKeyDialog();
// Memoized search results
const searchResults = useMemo(() => {
if (!searchValue) {
return null;
}
const allItems = [...modelSpecs, ...mappedEndpoints];
return filterItems(allItems, searchValue, agentsMap, assistantsMap || {});
}, [searchValue, modelSpecs, mappedEndpoints, agentsMap, assistantsMap]);
// Functions
const setDebouncedSearchValue = useMemo(
() =>
debounce((value: string) => {
setSearchValueState(value);
}, 200),
[],
);
const setEndpointSearchValue = (endpoint: string, value: string) => {
setEndpointSearchValues((prev) => ({
...prev,
[endpoint]: value,
}));
};
const handleSelectSpec = (spec: t.TModelSpec) => {
let model = spec.preset.model ?? null;
onSelectSpec?.(spec);
if (isAgentsEndpoint(spec.preset.endpoint)) {
model = spec.preset.agent_id ?? '';
} else if (isAssistantsEndpoint(spec.preset.endpoint)) {
model = spec.preset.assistant_id ?? '';
}
setSelectedValues({
endpoint: spec.preset.endpoint,
model,
modelSpec: spec.name,
});
};
const handleSelectEndpoint = (endpoint: Endpoint) => {
if (!endpoint.hasModels) {
if (endpoint.value) {
onSelectEndpoint?.(endpoint.value);
}
setSelectedValues({
endpoint: endpoint.value,
model: '',
modelSpec: '',
});
}
};
const handleSelectModel = (endpoint: Endpoint, model: string) => {
if (isAgentsEndpoint(endpoint.value)) {
onSelectEndpoint?.(endpoint.value, {
agent_id: model,
model: agentsMap?.[model]?.model ?? '',
});
} else if (isAssistantsEndpoint(endpoint.value)) {
onSelectEndpoint?.(endpoint.value, {
assistant_id: model,
model: assistantsMap?.[endpoint.value]?.[model]?.model ?? '',
});
} else if (endpoint.value) {
onSelectEndpoint?.(endpoint.value, { model });
}
setSelectedValues({
endpoint: endpoint.value,
model,
modelSpec: '',
});
};
const value = {
// State
searchValue,
searchResults,
selectedValues,
endpointSearchValues,
// LibreChat
agentsMap,
modelSpecs,
assistantsMap,
mappedEndpoints,
endpointsConfig,
// Functions
handleSelectSpec,
handleSelectModel,
setSelectedValues,
handleSelectEndpoint,
setEndpointSearchValue,
endpointRequiresUserKey,
setSearchValue: setDebouncedSearchValue,
// Dialog
...keyProps,
};
return <ModelSelectorContext.Provider value={value}>{children}</ModelSelectorContext.Provider>;
}