🥷 fix: Correct Agents Handling for Marketplace Users (#9065)

* refactor: Introduce ModelSelectorChatContext and integrate with ModelSelector

* fix: agents handling in ModelSelector to show expected agents if user has marketplace access
This commit is contained in:
Danny Avila 2025-08-14 19:13:48 -04:00 committed by GitHub
parent e4e25aaf2b
commit d57e7aec73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 124 additions and 57 deletions

View file

@ -1,6 +1,7 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import type { ModelSelectorProps } from '~/common'; import type { ModelSelectorProps } from '~/common';
import { ModelSelectorProvider, useModelSelectorContext } from './ModelSelectorContext'; import { ModelSelectorProvider, useModelSelectorContext } from './ModelSelectorContext';
import { ModelSelectorChatProvider } from './ModelSelectorChatContext';
import { renderModelSpecs, renderEndpoints, renderSearchResults } from './components'; import { renderModelSpecs, renderEndpoints, renderSearchResults } from './components';
import { getSelectedIcon, getDisplayValue } from './utils'; import { getSelectedIcon, getDisplayValue } from './utils';
import { CustomMenu as Menu } from './CustomMenu'; import { CustomMenu as Menu } from './CustomMenu';
@ -12,6 +13,7 @@ function ModelSelectorContent() {
const { const {
// LibreChat // LibreChat
agentsMap,
modelSpecs, modelSpecs,
mappedEndpoints, mappedEndpoints,
endpointsConfig, endpointsConfig,
@ -43,11 +45,12 @@ function ModelSelectorContent() {
() => () =>
getDisplayValue({ getDisplayValue({
localize, localize,
agentsMap,
modelSpecs, modelSpecs,
selectedValues, selectedValues,
mappedEndpoints, mappedEndpoints,
}), }),
[localize, modelSpecs, selectedValues, mappedEndpoints], [localize, agentsMap, modelSpecs, selectedValues, mappedEndpoints],
); );
const trigger = ( const trigger = (
@ -100,8 +103,10 @@ function ModelSelectorContent() {
export default function ModelSelector({ startupConfig }: ModelSelectorProps) { export default function ModelSelector({ startupConfig }: ModelSelectorProps) {
return ( return (
<ModelSelectorProvider startupConfig={startupConfig}> <ModelSelectorChatProvider>
<ModelSelectorContent /> <ModelSelectorProvider startupConfig={startupConfig}>
</ModelSelectorProvider> <ModelSelectorContent />
</ModelSelectorProvider>
</ModelSelectorChatProvider>
); );
} }

View file

@ -0,0 +1,54 @@
import React, { createContext, useContext, useMemo } from 'react';
import type { EModelEndpoint } from 'librechat-data-provider';
import { useChatContext } from '~/Providers/ChatContext';
interface ModelSelectorChatContextValue {
endpoint?: EModelEndpoint | null;
model?: string | null;
spec?: string | null;
agent_id?: string | null;
assistant_id?: string | null;
newConversation: ReturnType<typeof useChatContext>['newConversation'];
}
const ModelSelectorChatContext = createContext<ModelSelectorChatContextValue | undefined>(
undefined,
);
export function ModelSelectorChatProvider({ children }: { children: React.ReactNode }) {
const { conversation, newConversation } = useChatContext();
/** Context value only created when relevant conversation properties change */
const contextValue = useMemo<ModelSelectorChatContextValue>(
() => ({
endpoint: conversation?.endpoint,
model: conversation?.model,
spec: conversation?.spec,
agent_id: conversation?.agent_id,
assistant_id: conversation?.assistant_id,
newConversation,
}),
[
conversation?.endpoint,
conversation?.model,
conversation?.spec,
conversation?.agent_id,
conversation?.assistant_id,
newConversation,
],
);
return (
<ModelSelectorChatContext.Provider value={contextValue}>
{children}
</ModelSelectorChatContext.Provider>
);
}
export function useModelSelectorChatContext() {
const context = useContext(ModelSelectorChatContext);
if (!context) {
throw new Error('useModelSelectorChatContext must be used within ModelSelectorChatProvider');
}
return context;
}

View file

@ -3,10 +3,16 @@ import React, { createContext, useContext, useState, useMemo } from 'react';
import { isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider'; import { isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import type * as t from 'librechat-data-provider'; import type * as t from 'librechat-data-provider';
import type { Endpoint, SelectedValues } from '~/common'; import type { Endpoint, SelectedValues } from '~/common';
import { useAgentsMapContext, useAssistantsMapContext, useChatContext } from '~/Providers'; import {
import { useEndpoints, useSelectorEffects, useKeyDialog } from '~/hooks'; useAgentDefaultPermissionLevel,
useSelectorEffects,
useKeyDialog,
useEndpoints,
} from '~/hooks';
import { useAgentsMapContext, useAssistantsMapContext } from '~/Providers';
import { useGetEndpointsQuery, useListAgentsQuery } from '~/data-provider';
import { useModelSelectorChatContext } from './ModelSelectorChatContext';
import useSelectMention from '~/hooks/Input/useSelectMention'; import useSelectMention from '~/hooks/Input/useSelectMention';
import { useGetEndpointsQuery } from '~/data-provider';
import { filterItems } from './utils'; import { filterItems } from './utils';
type ModelSelectorContextType = { type ModelSelectorContextType = {
@ -51,14 +57,24 @@ export function ModelSelectorProvider({ children, startupConfig }: ModelSelector
const agentsMap = useAgentsMapContext(); const agentsMap = useAgentsMapContext();
const assistantsMap = useAssistantsMapContext(); const assistantsMap = useAssistantsMapContext();
const { data: endpointsConfig } = useGetEndpointsQuery(); const { data: endpointsConfig } = useGetEndpointsQuery();
const { conversation, newConversation } = useChatContext(); const { endpoint, model, spec, agent_id, assistant_id, newConversation } =
useModelSelectorChatContext();
const modelSpecs = useMemo(() => startupConfig?.modelSpecs?.list ?? [], [startupConfig]); const modelSpecs = useMemo(() => startupConfig?.modelSpecs?.list ?? [], [startupConfig]);
const permissionLevel = useAgentDefaultPermissionLevel();
const { data: agents = null } = useListAgentsQuery(
{ requiredPermission: permissionLevel },
{
select: (data) => data?.data,
},
);
const { mappedEndpoints, endpointRequiresUserKey } = useEndpoints({ const { mappedEndpoints, endpointRequiresUserKey } = useEndpoints({
agentsMap, agents,
assistantsMap, assistantsMap,
startupConfig, startupConfig,
endpointsConfig, endpointsConfig,
}); });
const { onSelectEndpoint, onSelectSpec } = useSelectMention({ const { onSelectEndpoint, onSelectSpec } = useSelectMention({
// presets, // presets,
modelSpecs, modelSpecs,
@ -70,13 +86,21 @@ export function ModelSelectorProvider({ children, startupConfig }: ModelSelector
// State // State
const [selectedValues, setSelectedValues] = useState<SelectedValues>({ const [selectedValues, setSelectedValues] = useState<SelectedValues>({
endpoint: conversation?.endpoint || '', endpoint: endpoint || '',
model: conversation?.model || '', model: model || '',
modelSpec: conversation?.spec || '', modelSpec: spec || '',
}); });
useSelectorEffects({ useSelectorEffects({
agentsMap, agentsMap,
conversation, conversation: endpoint
? ({
endpoint: endpoint ?? null,
model: model ?? null,
spec: spec ?? null,
agent_id: agent_id ?? null,
assistant_id: assistant_id ?? null,
} as any)
: null,
assistantsMap, assistantsMap,
setSelectedValues, setSelectedValues,
}); });
@ -86,7 +110,7 @@ export function ModelSelectorProvider({ children, startupConfig }: ModelSelector
const keyProps = useKeyDialog(); const keyProps = useKeyDialog();
// Memoized search results /** Memoized search results */
const searchResults = useMemo(() => { const searchResults = useMemo(() => {
if (!searchValue) { if (!searchValue) {
return null; return null;
@ -95,7 +119,6 @@ export function ModelSelectorProvider({ children, startupConfig }: ModelSelector
return filterItems(allItems, searchValue, agentsMap, assistantsMap || {}); return filterItems(allItems, searchValue, agentsMap, assistantsMap || {});
}, [searchValue, modelSpecs, mappedEndpoints, agentsMap, assistantsMap]); }, [searchValue, modelSpecs, mappedEndpoints, agentsMap, assistantsMap]);
// Functions
const setDebouncedSearchValue = useMemo( const setDebouncedSearchValue = useMemo(
() => () =>
debounce((value: string) => { debounce((value: string) => {

View file

@ -167,11 +167,13 @@ export const getDisplayValue = ({
mappedEndpoints, mappedEndpoints,
selectedValues, selectedValues,
modelSpecs, modelSpecs,
agentsMap,
}: { }: {
localize: ReturnType<typeof useLocalize>; localize: ReturnType<typeof useLocalize>;
selectedValues: SelectedValues; selectedValues: SelectedValues;
mappedEndpoints: Endpoint[]; mappedEndpoints: Endpoint[];
modelSpecs: TModelSpec[]; modelSpecs: TModelSpec[];
agentsMap?: TAgentsMap;
}) => { }) => {
if (selectedValues.modelSpec) { if (selectedValues.modelSpec) {
const spec = modelSpecs.find((s) => s.name === selectedValues.modelSpec); const spec = modelSpecs.find((s) => s.name === selectedValues.modelSpec);
@ -190,6 +192,9 @@ export const getDisplayValue = ({
endpoint.agentNames[selectedValues.model] endpoint.agentNames[selectedValues.model]
) { ) {
return endpoint.agentNames[selectedValues.model]; return endpoint.agentNames[selectedValues.model];
} else if (isAgentsEndpoint(endpoint.value) && agentsMap) {
const agent = agentsMap[selectedValues.model];
return agent?.name || selectedValues.model;
} }
if ( if (

View file

@ -7,8 +7,8 @@ import type { UseMutationResult, QueryObserverResult } from '@tanstack/react-que
import type { Agent, AgentCreateParams } from 'librechat-data-provider'; import type { Agent, AgentCreateParams } from 'librechat-data-provider';
import type { TAgentCapabilities, AgentForm } from '~/common'; import type { TAgentCapabilities, AgentForm } from '~/common';
import { cn, createProviderOption, processAgentOption, getDefaultAgentFormValues } from '~/utils'; import { cn, createProviderOption, processAgentOption, getDefaultAgentFormValues } from '~/utils';
import { useGetStartupConfig, useListAgentsQuery } from '~/data-provider';
import { useLocalize, useAgentDefaultPermissionLevel } from '~/hooks'; import { useLocalize, useAgentDefaultPermissionLevel } from '~/hooks';
import { useListAgentsQuery } from '~/data-provider';
const keys = new Set(Object.keys(defaultAgentFormValues)); const keys = new Set(Object.keys(defaultAgentFormValues));
@ -26,8 +26,6 @@ export default function AgentSelect({
const localize = useLocalize(); const localize = useLocalize();
const lastSelectedAgent = useRef<string | null>(null); const lastSelectedAgent = useRef<string | null>(null);
const { control, reset } = useFormContext(); const { control, reset } = useFormContext();
const { data: startupConfig } = useGetStartupConfig();
const permissionLevel = useAgentDefaultPermissionLevel(); const permissionLevel = useAgentDefaultPermissionLevel();
const { data: agents = null } = useListAgentsQuery( const { data: agents = null } = useListAgentsQuery(
@ -40,7 +38,6 @@ export default function AgentSelect({
...agent, ...agent,
name: agent.name || agent.id, name: agent.name || agent.id,
}, },
instanceProjectId: startupConfig?.instanceProjectId,
}), }),
), ),
}, },
@ -122,7 +119,7 @@ export default function AgentSelect({
reset(formValues); reset(formValues);
}, },
[reset, startupConfig], [reset],
); );
const onSelect = useCallback( const onSelect = useCallback(

View file

@ -9,7 +9,7 @@ export default function useAgentsMap({
}: { }: {
isAuthenticated: boolean; isAuthenticated: boolean;
}): TAgentsMap | undefined { }): TAgentsMap | undefined {
const { data: agentsList = null } = useListAgentsQuery( const { data: mappedAgents = null } = useListAgentsQuery(
{ requiredPermission: PermissionBits.VIEW }, { requiredPermission: PermissionBits.VIEW },
{ {
select: (res) => mapAgents(res.data), select: (res) => mapAgents(res.data),
@ -17,9 +17,9 @@ export default function useAgentsMap({
}, },
); );
const agents = useMemo<TAgentsMap | undefined>(() => { const agentsMap = useMemo<TAgentsMap | undefined>(() => {
return agentsList !== null ? agentsList : undefined; return mappedAgents !== null ? mappedAgents : undefined;
}, [agentsList]); }, [mappedAgents]);
return agents; return agentsMap;
} }

View file

@ -1,71 +1,56 @@
import React, { useMemo, useCallback } from 'react'; import React, { useMemo, useCallback } from 'react';
import { useGetModelsQuery } from 'librechat-data-provider/react-query'; import { useGetModelsQuery } from 'librechat-data-provider/react-query';
import { import {
EModelEndpoint,
PermissionTypes,
Permissions, Permissions,
alternateName, alternateName,
EModelEndpoint,
PermissionTypes,
} from 'librechat-data-provider'; } from 'librechat-data-provider';
import type { import type {
Agent,
Assistant,
TEndpointsConfig, TEndpointsConfig,
TAgentsMap,
TAssistantsMap, TAssistantsMap,
TStartupConfig, TStartupConfig,
Assistant,
Agent,
} from 'librechat-data-provider'; } from 'librechat-data-provider';
import type { Endpoint } from '~/common'; import type { Endpoint } from '~/common';
import { mapEndpoints, getIconKey, getEndpointField } from '~/utils'; import { mapEndpoints, getIconKey, getEndpointField } from '~/utils';
import { useGetEndpointsQuery } from '~/data-provider'; import { useGetEndpointsQuery } from '~/data-provider';
import { useChatContext } from '~/Providers';
import { useHasAccess } from '~/hooks'; import { useHasAccess } from '~/hooks';
import { icons } from './Icons'; import { icons } from './Icons';
export const useEndpoints = ({ export const useEndpoints = ({
agentsMap, agents,
assistantsMap, assistantsMap,
endpointsConfig, endpointsConfig,
startupConfig, startupConfig,
}: { }: {
agentsMap?: TAgentsMap; agents?: Agent[] | null;
assistantsMap?: TAssistantsMap; assistantsMap?: TAssistantsMap;
endpointsConfig: TEndpointsConfig; endpointsConfig: TEndpointsConfig;
startupConfig: TStartupConfig | undefined; startupConfig: TStartupConfig | undefined;
}) => { }) => {
const modelsQuery = useGetModelsQuery(); const modelsQuery = useGetModelsQuery();
const { conversation } = useChatContext();
const { data: endpoints = [] } = useGetEndpointsQuery({ select: mapEndpoints }); const { data: endpoints = [] } = useGetEndpointsQuery({ select: mapEndpoints });
const { instanceProjectId } = startupConfig ?? {};
const interfaceConfig = startupConfig?.interface ?? {}; const interfaceConfig = startupConfig?.interface ?? {};
const includedEndpoints = useMemo( const includedEndpoints = useMemo(
() => new Set(startupConfig?.modelSpecs?.addedEndpoints ?? []), () => new Set(startupConfig?.modelSpecs?.addedEndpoints ?? []),
[startupConfig?.modelSpecs?.addedEndpoints], [startupConfig?.modelSpecs?.addedEndpoints],
); );
const { endpoint } = conversation ?? {};
const hasAgentAccess = useHasAccess({ const hasAgentAccess = useHasAccess({
permissionType: PermissionTypes.AGENTS, permissionType: PermissionTypes.AGENTS,
permission: Permissions.USE, permission: Permissions.USE,
}); });
const agents = useMemo(
() =>
Object.values(agentsMap ?? {}).filter(
(agent): agent is Agent & { name: string } =>
agent !== undefined && 'id' in agent && 'name' in agent && agent.name !== null,
),
[agentsMap],
);
const assistants: Assistant[] = useMemo( const assistants: Assistant[] = useMemo(
() => Object.values(assistantsMap?.[EModelEndpoint.assistants] ?? {}), () => Object.values(assistantsMap?.[EModelEndpoint.assistants] ?? {}),
[endpoint, assistantsMap], [assistantsMap],
); );
const azureAssistants: Assistant[] = useMemo( const azureAssistants: Assistant[] = useMemo(
() => Object.values(assistantsMap?.[EModelEndpoint.azureAssistants] ?? {}), () => Object.values(assistantsMap?.[EModelEndpoint.azureAssistants] ?? {}),
[endpoint, assistantsMap], [assistantsMap],
); );
const filteredEndpoints = useMemo(() => { const filteredEndpoints = useMemo(() => {
@ -84,7 +69,7 @@ export const useEndpoints = ({
} }
return result; return result;
}, [endpoints, hasAgentAccess, includedEndpoints]); }, [endpoints, hasAgentAccess, includedEndpoints, interfaceConfig.modelSelect]);
const endpointRequiresUserKey = useCallback( const endpointRequiresUserKey = useCallback(
(ep: string) => { (ep: string) => {
@ -100,7 +85,7 @@ export const useEndpoints = ({
const Icon = icons[iconKey]; const Icon = icons[iconKey];
const endpointIconURL = getEndpointField(endpointsConfig, ep, 'iconURL'); const endpointIconURL = getEndpointField(endpointsConfig, ep, 'iconURL');
const hasModels = const hasModels =
(ep === EModelEndpoint.agents && agents?.length > 0) || (ep === EModelEndpoint.agents && (agents?.length ?? 0) > 0) ||
(ep === EModelEndpoint.assistants && assistants?.length > 0) || (ep === EModelEndpoint.assistants && assistants?.length > 0) ||
(ep !== EModelEndpoint.assistants && (ep !== EModelEndpoint.assistants &&
ep !== EModelEndpoint.agents && ep !== EModelEndpoint.agents &&
@ -122,16 +107,16 @@ export const useEndpoints = ({
}; };
// Handle agents case // Handle agents case
if (ep === EModelEndpoint.agents && agents.length > 0) { if (ep === EModelEndpoint.agents && (agents?.length ?? 0) > 0) {
result.models = agents.map((agent) => ({ result.models = agents?.map((agent) => ({
name: agent.id, name: agent.id,
isGlobal: agent.isPublic ?? false, isGlobal: agent.isPublic ?? false,
})); }));
result.agentNames = agents.reduce((acc, agent) => { result.agentNames = agents?.reduce((acc, agent) => {
acc[agent.id] = agent.name || ''; acc[agent.id] = agent.name || '';
return acc; return acc;
}, {}); }, {});
result.modelIcons = agents.reduce((acc, agent) => { result.modelIcons = agents?.reduce((acc, agent) => {
acc[agent.id] = agent?.avatar?.filepath; acc[agent.id] = agent?.avatar?.filepath;
return acc; return acc;
}, {}); }, {});
@ -192,7 +177,7 @@ export const useEndpoints = ({
return result; return result;
}); });
}, [filteredEndpoints, endpointsConfig, modelsQuery.data, agents, assistants]); }, [filteredEndpoints, endpointsConfig, modelsQuery.data, agents, assistants, azureAssistants]);
return { return {
mappedEndpoints, mappedEndpoints,

View file

@ -57,11 +57,9 @@ export const getDefaultAgentFormValues = () => ({
export const processAgentOption = ({ export const processAgentOption = ({
agent: _agent, agent: _agent,
fileMap, fileMap,
instanceProjectId,
}: { }: {
agent?: Agent; agent?: Agent;
fileMap?: Record<string, TFile | undefined>; fileMap?: Record<string, TFile | undefined>;
instanceProjectId?: string;
}): TAgentOption => { }): TAgentOption => {
const isGlobal = _agent?.isPublic ?? false; const isGlobal = _agent?.isPublic ?? false;
const agent: TAgentOption = { const agent: TAgentOption = {