📛 feat: Chat Badges via Model Specs (#10272)

* refactor: remove `useChatContext` from `useSelectMention`, explicitly pass `conversation` object

* feat: ephemeral agents via model specs

* refactor: Sync Jotai state with ephemeral agent state, also when Ephemeral Agent has no MCP servers selected

* refactor: move `useUpdateEphemeralAgent` to store and clean up imports

* refactor: reorder imports and invalidate queries for mcpConnectionStatus in event handler

* refactor: replace useApplyModelSpecEffects with useApplyModelSpecAgents and update event handlers to use new agent template logic

* ci: update useMCPSelect test to verify mcpValues sync with empty ephemeralAgent.mcp
This commit is contained in:
Danny Avila 2025-10-27 19:46:30 -04:00 committed by GitHub
parent 64df54528d
commit 33d6b337bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 254 additions and 41 deletions

View file

@ -62,25 +62,37 @@ const getAgents = async (searchParameter) => await Agent.find(searchParameter).l
* *
* @param {Object} params * @param {Object} params
* @param {ServerRequest} params.req * @param {ServerRequest} params.req
* @param {string} params.spec
* @param {string} params.agent_id * @param {string} params.agent_id
* @param {string} params.endpoint * @param {string} params.endpoint
* @param {import('@librechat/agents').ClientOptions} [params.model_parameters] * @param {import('@librechat/agents').ClientOptions} [params.model_parameters]
* @returns {Promise<Agent|null>} The agent document as a plain object, or null if not found. * @returns {Promise<Agent|null>} The agent document as a plain object, or null if not found.
*/ */
const loadEphemeralAgent = async ({ req, agent_id, endpoint, model_parameters: _m }) => { const loadEphemeralAgent = async ({ req, spec, agent_id, endpoint, model_parameters: _m }) => {
const { model, ...model_parameters } = _m; const { model, ...model_parameters } = _m;
const modelSpecs = req.config?.modelSpecs?.list;
/** @type {TModelSpec | null} */
let modelSpec = null;
if (spec != null && spec !== '') {
modelSpec = modelSpecs?.find((s) => s.name === spec) || null;
}
/** @type {TEphemeralAgent | null} */ /** @type {TEphemeralAgent | null} */
const ephemeralAgent = req.body.ephemeralAgent; const ephemeralAgent = req.body.ephemeralAgent;
const mcpServers = new Set(ephemeralAgent?.mcp); const mcpServers = new Set(ephemeralAgent?.mcp);
if (modelSpec?.mcpServers) {
for (const mcpServer of modelSpec.mcpServers) {
mcpServers.add(mcpServer);
}
}
/** @type {string[]} */ /** @type {string[]} */
const tools = []; const tools = [];
if (ephemeralAgent?.execute_code === true) { if (ephemeralAgent?.execute_code === true || modelSpec?.executeCode === true) {
tools.push(Tools.execute_code); tools.push(Tools.execute_code);
} }
if (ephemeralAgent?.file_search === true) { if (ephemeralAgent?.file_search === true || modelSpec?.fileSearch === true) {
tools.push(Tools.file_search); tools.push(Tools.file_search);
} }
if (ephemeralAgent?.web_search === true) { if (ephemeralAgent?.web_search === true || modelSpec?.webSearch === true) {
tools.push(Tools.web_search); tools.push(Tools.web_search);
} }
@ -122,17 +134,18 @@ const loadEphemeralAgent = async ({ req, agent_id, endpoint, model_parameters: _
* *
* @param {Object} params * @param {Object} params
* @param {ServerRequest} params.req * @param {ServerRequest} params.req
* @param {string} params.spec
* @param {string} params.agent_id * @param {string} params.agent_id
* @param {string} params.endpoint * @param {string} params.endpoint
* @param {import('@librechat/agents').ClientOptions} [params.model_parameters] * @param {import('@librechat/agents').ClientOptions} [params.model_parameters]
* @returns {Promise<Agent|null>} The agent document as a plain object, or null if not found. * @returns {Promise<Agent|null>} The agent document as a plain object, or null if not found.
*/ */
const loadAgent = async ({ req, agent_id, endpoint, model_parameters }) => { const loadAgent = async ({ req, spec, agent_id, endpoint, model_parameters }) => {
if (!agent_id) { if (!agent_id) {
return null; return null;
} }
if (agent_id === EPHEMERAL_AGENT_ID) { if (agent_id === EPHEMERAL_AGENT_ID) {
return await loadEphemeralAgent({ req, agent_id, endpoint, model_parameters }); return await loadEphemeralAgent({ req, spec, agent_id, endpoint, model_parameters });
} }
const agent = await getAgent({ const agent = await getAgent({
id: agent_id, id: agent_id,

View file

@ -3,9 +3,10 @@ const { isAgentsEndpoint, removeNullishValues, Constants } = require('librechat-
const { loadAgent } = require('~/models/Agent'); const { loadAgent } = require('~/models/Agent');
const buildOptions = (req, endpoint, parsedBody, endpointType) => { const buildOptions = (req, endpoint, parsedBody, endpointType) => {
const { spec, iconURL, agent_id, instructions, ...model_parameters } = parsedBody; const { spec, iconURL, agent_id, ...model_parameters } = parsedBody;
const agentPromise = loadAgent({ const agentPromise = loadAgent({
req, req,
spec,
agent_id: isAgentsEndpoint(endpoint) ? agent_id : Constants.EPHEMERAL_AGENT_ID, agent_id: isAgentsEndpoint(endpoint) ? agent_id : Constants.EPHEMERAL_AGENT_ID,
endpoint, endpoint,
model_parameters, model_parameters,
@ -20,7 +21,6 @@ const buildOptions = (req, endpoint, parsedBody, endpointType) => {
endpoint, endpoint,
agent_id, agent_id,
endpointType, endpointType,
instructions,
model_parameters, model_parameters,
agent: agentPromise, agent: agentPromise,
}); });

View file

@ -220,6 +220,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
<div className={cn('flex w-full items-center', isRTL && 'flex-row-reverse')}> <div className={cn('flex w-full items-center', isRTL && 'flex-row-reverse')}>
{showPlusPopover && !isAssistantsEndpoint(endpoint) && ( {showPlusPopover && !isAssistantsEndpoint(endpoint) && (
<Mention <Mention
conversation={conversation}
setShowMentionPopover={setShowPlusPopover} setShowMentionPopover={setShowPlusPopover}
newConversation={generateConversation} newConversation={generateConversation}
textAreaRef={textAreaRef} textAreaRef={textAreaRef}
@ -230,6 +231,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
)} )}
{showMentionPopover && ( {showMentionPopover && (
<Mention <Mention
conversation={conversation}
setShowMentionPopover={setShowMentionPopover} setShowMentionPopover={setShowMentionPopover}
newConversation={newConversation} newConversation={newConversation}
textAreaRef={textAreaRef} textAreaRef={textAreaRef}

View file

@ -2,6 +2,7 @@ import { useState, useRef, useEffect } from 'react';
import { useCombobox } from '@librechat/client'; import { useCombobox } from '@librechat/client';
import { AutoSizer, List } from 'react-virtualized'; import { AutoSizer, List } from 'react-virtualized';
import { EModelEndpoint } from 'librechat-data-provider'; import { EModelEndpoint } from 'librechat-data-provider';
import type { TConversation } from 'librechat-data-provider';
import type { MentionOption, ConvoGenerator } from '~/common'; import type { MentionOption, ConvoGenerator } from '~/common';
import type { SetterOrUpdater } from 'recoil'; import type { SetterOrUpdater } from 'recoil';
import useSelectMention from '~/hooks/Input/useSelectMention'; import useSelectMention from '~/hooks/Input/useSelectMention';
@ -14,6 +15,7 @@ import MentionItem from './MentionItem';
const ROW_HEIGHT = 40; const ROW_HEIGHT = 40;
export default function Mention({ export default function Mention({
conversation,
setShowMentionPopover, setShowMentionPopover,
newConversation, newConversation,
textAreaRef, textAreaRef,
@ -21,6 +23,7 @@ export default function Mention({
placeholder = 'com_ui_mention', placeholder = 'com_ui_mention',
includeAssistants = true, includeAssistants = true,
}: { }: {
conversation: TConversation | null;
setShowMentionPopover: SetterOrUpdater<boolean>; setShowMentionPopover: SetterOrUpdater<boolean>;
newConversation: ConvoGenerator; newConversation: ConvoGenerator;
textAreaRef: React.MutableRefObject<HTMLTextAreaElement | null>; textAreaRef: React.MutableRefObject<HTMLTextAreaElement | null>;
@ -42,6 +45,7 @@ export default function Mention({
const { onSelectMention } = useSelectMention({ const { onSelectMention } = useSelectMention({
presets, presets,
modelSpecs, modelSpecs,
conversation,
assistantsMap, assistantsMap,
endpointsConfig, endpointsConfig,
newConversation, newConversation,

View file

@ -1,5 +1,5 @@
import React, { createContext, useContext, useMemo } from 'react'; import React, { createContext, useContext, useMemo } from 'react';
import type { EModelEndpoint } from 'librechat-data-provider'; import type { EModelEndpoint, TConversation } from 'librechat-data-provider';
import { useChatContext } from '~/Providers/ChatContext'; import { useChatContext } from '~/Providers/ChatContext';
interface ModelSelectorChatContextValue { interface ModelSelectorChatContextValue {
@ -8,6 +8,7 @@ interface ModelSelectorChatContextValue {
spec?: string | null; spec?: string | null;
agent_id?: string | null; agent_id?: string | null;
assistant_id?: string | null; assistant_id?: string | null;
conversation: TConversation | null;
newConversation: ReturnType<typeof useChatContext>['newConversation']; newConversation: ReturnType<typeof useChatContext>['newConversation'];
} }
@ -26,16 +27,10 @@ export function ModelSelectorChatProvider({ children }: { children: React.ReactN
spec: conversation?.spec, spec: conversation?.spec,
agent_id: conversation?.agent_id, agent_id: conversation?.agent_id,
assistant_id: conversation?.assistant_id, assistant_id: conversation?.assistant_id,
conversation,
newConversation, newConversation,
}), }),
[ [conversation, newConversation],
conversation?.endpoint,
conversation?.model,
conversation?.spec,
conversation?.agent_id,
conversation?.assistant_id,
newConversation,
],
); );
return ( return (

View file

@ -57,7 +57,7 @@ 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 { endpoint, model, spec, agent_id, assistant_id, newConversation } = const { endpoint, model, spec, agent_id, assistant_id, conversation, newConversation } =
useModelSelectorChatContext(); useModelSelectorChatContext();
const modelSpecs = useMemo(() => { const modelSpecs = useMemo(() => {
const specs = startupConfig?.modelSpecs?.list ?? []; const specs = startupConfig?.modelSpecs?.list ?? [];
@ -96,6 +96,7 @@ export function ModelSelectorProvider({ children, startupConfig }: ModelSelector
const { onSelectEndpoint, onSelectSpec } = useSelectMention({ const { onSelectEndpoint, onSelectSpec } = useSelectMention({
// presets, // presets,
modelSpecs, modelSpecs,
conversation,
assistantsMap, assistantsMap,
endpointsConfig, endpointsConfig,
newConversation, newConversation,

View file

@ -6,3 +6,4 @@ export { default as useAgentCapabilities } from './useAgentCapabilities';
export { default as useGetAgentsConfig } from './useGetAgentsConfig'; export { default as useGetAgentsConfig } from './useGetAgentsConfig';
export { default as useAgentDefaultPermissionLevel } from './useAgentDefaultPermissionLevel'; export { default as useAgentDefaultPermissionLevel } from './useAgentDefaultPermissionLevel';
export { default as useAgentToolPermissions } from './useAgentToolPermissions'; export { default as useAgentToolPermissions } from './useAgentToolPermissions';
export * from './useApplyModelSpecAgents';

View file

@ -0,0 +1,95 @@
import { useCallback } from 'react';
import type { TStartupConfig, TSubmission } from 'librechat-data-provider';
import { useUpdateEphemeralAgent, useApplyNewAgentTemplate } from '~/store/agents';
import { getModelSpec, applyModelSpecEphemeralAgent } from '~/utils';
/**
* Hook that applies a model spec from a preset to an ephemeral agent.
* This is used when initializing a new conversation with a preset that has a spec.
*/
export function useApplyModelSpecEffects() {
const updateEphemeralAgent = useUpdateEphemeralAgent();
const applyPresetModelSpec = useCallback(
({
convoId,
specName,
startupConfig,
}: {
convoId: string | null;
specName?: string | null;
startupConfig?: TStartupConfig;
}) => {
if (specName == null || !specName) {
return;
}
const modelSpec = getModelSpec({
specName,
startupConfig,
});
applyModelSpecEphemeralAgent({
convoId,
modelSpec,
updateEphemeralAgent,
});
},
[updateEphemeralAgent],
);
return applyPresetModelSpec;
}
export function useApplyAgentTemplate() {
const applyAgentTemplate = useApplyNewAgentTemplate();
/**
* Helper function to apply agent template with model spec merged into ephemeral agent
*/
const applyAgentTemplateWithSpec = useCallback(
({
targetId,
sourceId,
ephemeralAgent,
specName,
startupConfig,
}: {
targetId: string;
sourceId?: TSubmission['conversation']['conversationId'] | null;
ephemeralAgent: TSubmission['ephemeralAgent'];
specName?: string | null;
startupConfig?: TStartupConfig;
}) => {
if (!specName) {
applyAgentTemplate(targetId, sourceId, ephemeralAgent);
return;
}
const modelSpec = getModelSpec({
specName,
startupConfig,
});
if (!modelSpec) {
applyAgentTemplate(targetId, sourceId, ephemeralAgent);
return;
}
// Merge model spec fields into ephemeral agent
const mergedAgent = {
...ephemeralAgent,
mcp: [...(ephemeralAgent?.mcp ?? []), ...(modelSpec.mcpServers ?? [])],
web_search: ephemeralAgent?.web_search ?? modelSpec.webSearch ?? false,
file_search: ephemeralAgent?.file_search ?? modelSpec.fileSearch ?? false,
execute_code: ephemeralAgent?.execute_code ?? modelSpec.executeCode ?? false,
};
// Deduplicate MCP servers
mergedAgent.mcp = [...new Set(mergedAgent.mcp)];
applyAgentTemplate(targetId, sourceId, mergedAgent);
},
[applyAgentTemplate],
);
return applyAgentTemplateWithSpec;
}

View file

@ -1,8 +1,14 @@
import { useCallback } from 'react';
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, Constants, dataService } from 'librechat-data-provider'; import { QueryKeys, Constants, dataService } from 'librechat-data-provider';
import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider'; import type {
TEndpointsConfig,
TStartupConfig,
TModelsConfig,
TConversation,
} from 'librechat-data-provider';
import { import {
getDefaultEndpoint, getDefaultEndpoint,
clearMessagesCache, clearMessagesCache,
@ -10,15 +16,34 @@ import {
getEndpointField, getEndpointField,
logger, logger,
} from '~/utils'; } from '~/utils';
import { useApplyModelSpecEffects } from '~/hooks/Agents';
import store from '~/store'; import store from '~/store';
const useNavigateToConvo = (index = 0) => { const useNavigateToConvo = (index = 0) => {
const navigate = useNavigate(); const navigate = useNavigate();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const clearAllConversations = store.useClearConvoState(); const clearAllConversations = store.useClearConvoState();
const applyModelSpecEffects = useApplyModelSpecEffects();
const setSubmission = useSetRecoilState(store.submissionByIndex(index)); const setSubmission = useSetRecoilState(store.submissionByIndex(index));
const clearAllLatestMessages = store.useClearLatestMessages(`useNavigateToConvo ${index}`); const clearAllLatestMessages = store.useClearLatestMessages(`useNavigateToConvo ${index}`);
const { hasSetConversation, setConversation } = store.useCreateConversationAtom(index); const { hasSetConversation, setConversation: setConvo } = store.useCreateConversationAtom(index);
const setConversation = useCallback(
(conversation: TConversation) => {
setConvo(conversation);
if (!conversation.spec) {
return;
}
const startupConfig = queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]);
applyModelSpecEffects({
startupConfig,
specName: conversation?.spec,
convoId: conversation.conversationId,
});
},
[setConvo, queryClient, applyModelSpecEffects],
);
const fetchFreshData = async (conversation?: Partial<TConversation>) => { const fetchFreshData = async (conversation?: Partial<TConversation>) => {
const conversationId = conversation?.conversationId; const conversationId = conversation?.conversationId;

View file

@ -10,18 +10,19 @@ import type {
} from 'librechat-data-provider'; } from 'librechat-data-provider';
import type { MentionOption, ConvoGenerator } from '~/common'; import type { MentionOption, ConvoGenerator } from '~/common';
import { getConvoSwitchLogic, getModelSpecIconURL, removeUnavailableTools, logger } from '~/utils'; import { getConvoSwitchLogic, getModelSpecIconURL, removeUnavailableTools, logger } from '~/utils';
import { useChatContext } from '~/Providers';
import { useDefaultConvo } from '~/hooks'; import { useDefaultConvo } from '~/hooks';
import store from '~/store'; import store from '~/store';
export default function useSelectMention({ export default function useSelectMention({
presets, presets,
modelSpecs, modelSpecs,
conversation,
assistantsMap, assistantsMap,
returnHandlers,
endpointsConfig, endpointsConfig,
newConversation, newConversation,
returnHandlers,
}: { }: {
conversation: TConversation | null;
presets?: TPreset[]; presets?: TPreset[];
modelSpecs: TModelSpec[]; modelSpecs: TModelSpec[];
assistantsMap?: TAssistantsMap; assistantsMap?: TAssistantsMap;
@ -29,7 +30,6 @@ export default function useSelectMention({
endpointsConfig: TEndpointsConfig; endpointsConfig: TEndpointsConfig;
returnHandlers?: boolean; returnHandlers?: boolean;
}) { }) {
const { conversation } = useChatContext();
const getDefaultConversation = useDefaultConvo(); const getDefaultConversation = useDefaultConvo();
const modularChat = useRecoilValue(store.modularChat); const modularChat = useRecoilValue(store.modularChat);
const availableTools = useRecoilValue(store.availableTools); const availableTools = useRecoilValue(store.availableTools);

View file

@ -431,9 +431,10 @@ describe('useMCPSelect', () => {
}); });
}); });
// Values should remain unchanged since empty mcp array doesn't trigger update // Values should sync to empty array when ephemeralAgent.mcp is set to []
// (due to the condition: ephemeralAgent?.mcp && ephemeralAgent.mcp.length > 0) await waitFor(() => {
expect(result.current.mcpHook.mcpValues).toEqual(['initial-value']); expect(result.current.mcpHook.mcpValues).toEqual([]);
});
}); });
it('should properly sync non-empty arrays from ephemeralAgent', async () => { it('should properly sync non-empty arrays from ephemeralAgent', async () => {

View file

@ -25,6 +25,8 @@ export function useMCPSelect({ conversationId }: { conversationId?: string | nul
// Strip out servers that are not available in the startup config // Strip out servers that are not available in the startup config
const activeMcps = mcps.filter((mcp) => configuredServers.has(mcp)); const activeMcps = mcps.filter((mcp) => configuredServers.has(mcp));
setMCPValuesRaw(activeMcps); setMCPValuesRaw(activeMcps);
} else {
setMCPValuesRaw([]);
} }
}, [ephemeralAgent?.mcp, setMCPValuesRaw, configuredServers]); }, [ephemeralAgent?.mcp, setMCPValuesRaw, configuredServers]);

View file

@ -1,8 +1,8 @@
import { v4 } from 'uuid';
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { v4 } from 'uuid';
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { import {
QueryKeys, QueryKeys,
Constants, Constants,
@ -13,7 +13,12 @@ import {
tConvoUpdateSchema, tConvoUpdateSchema,
isAssistantsEndpoint, isAssistantsEndpoint,
} from 'librechat-data-provider'; } from 'librechat-data-provider';
import type { TMessage, TConversation, EventSubmission } from 'librechat-data-provider'; import type {
TMessage,
TConversation,
EventSubmission,
TStartupConfig,
} from 'librechat-data-provider';
import type { TResData, TFinalResData, ConvoGenerator } from '~/common'; import type { TResData, TFinalResData, ConvoGenerator } from '~/common';
import type { InfiniteData } from '@tanstack/react-query'; import type { InfiniteData } from '@tanstack/react-query';
import type { TGenTitleMutation } from '~/data-provider'; import type { TGenTitleMutation } from '~/data-provider';
@ -31,11 +36,12 @@ import {
} from '~/utils'; } from '~/utils';
import useAttachmentHandler from '~/hooks/SSE/useAttachmentHandler'; import useAttachmentHandler from '~/hooks/SSE/useAttachmentHandler';
import useContentHandler from '~/hooks/SSE/useContentHandler'; import useContentHandler from '~/hooks/SSE/useContentHandler';
import store, { useApplyNewAgentTemplate } from '~/store';
import useStepHandler from '~/hooks/SSE/useStepHandler'; import useStepHandler from '~/hooks/SSE/useStepHandler';
import { useApplyAgentTemplate } from '~/hooks/Agents';
import { useAuthContext } from '~/hooks/AuthContext'; import { useAuthContext } from '~/hooks/AuthContext';
import { MESSAGE_UPDATE_INTERVAL } from '~/common'; import { MESSAGE_UPDATE_INTERVAL } from '~/common';
import { useLiveAnnouncer } from '~/Providers'; import { useLiveAnnouncer } from '~/Providers';
import store from '~/store';
type TSyncData = { type TSyncData = {
sync: boolean; sync: boolean;
@ -172,7 +178,7 @@ export default function useEventHandlers({
}: EventHandlerParams) { }: EventHandlerParams) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { announcePolite } = useLiveAnnouncer(); const { announcePolite } = useLiveAnnouncer();
const applyAgentTemplate = useApplyNewAgentTemplate(); const applyAgentTemplate = useApplyAgentTemplate();
const setAbortScroll = useSetRecoilState(store.abortScroll); const setAbortScroll = useSetRecoilState(store.abortScroll);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
@ -356,6 +362,7 @@ export default function useEventHandlers({
const createdHandler = useCallback( const createdHandler = useCallback(
(data: TResData, submission: EventSubmission) => { (data: TResData, submission: EventSubmission) => {
queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]);
const { messages, userMessage, isRegenerate = false, isTemporary = false } = submission; const { messages, userMessage, isRegenerate = false, isTemporary = false } = submission;
const initialResponse = { const initialResponse = {
...submission.initialResponse, ...submission.initialResponse,
@ -411,11 +418,13 @@ export default function useEventHandlers({
} }
if (conversationId) { if (conversationId) {
applyAgentTemplate( applyAgentTemplate({
conversationId, targetId: conversationId,
submission.conversation.conversationId, sourceId: submission.conversation?.conversationId,
submission.ephemeralAgent, ephemeralAgent: submission.ephemeralAgent,
); specName: submission.conversation?.spec,
startupConfig: queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]),
});
} }
if (resetLatestMessage) { if (resetLatestMessage) {
@ -566,11 +575,13 @@ export default function useEventHandlers({
}); });
if (conversation.conversationId && submission.ephemeralAgent) { if (conversation.conversationId && submission.ephemeralAgent) {
applyAgentTemplate( applyAgentTemplate({
conversation.conversationId, targetId: conversation.conversationId,
submissionConvo.conversationId, sourceId: submissionConvo.conversationId,
submission.ephemeralAgent, ephemeralAgent: submission.ephemeralAgent,
); specName: submission.conversation?.spec,
startupConfig: queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]),
});
} }
if (location.pathname === `/c/${Constants.NEW_CONVO}`) { if (location.pathname === `/c/${Constants.NEW_CONVO}`) {

View file

@ -29,6 +29,7 @@ import {
import { useDeleteFilesMutation, useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider'; import { useDeleteFilesMutation, useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
import useAssistantListMap from './Assistants/useAssistantListMap'; import useAssistantListMap from './Assistants/useAssistantListMap';
import { useResetChatBadges } from './useChatBadges'; import { useResetChatBadges } from './useChatBadges';
import { useApplyModelSpecEffects } from './Agents';
import { usePauseGlobalAudio } from './Audio'; import { usePauseGlobalAudio } from './Audio';
import { logger } from '~/utils'; import { logger } from '~/utils';
import store from '~/store'; import store from '~/store';
@ -37,6 +38,7 @@ const useNewConvo = (index = 0) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const { data: startupConfig } = useGetStartupConfig(); const { data: startupConfig } = useGetStartupConfig();
const applyModelSpecEffects = useApplyModelSpecEffects();
const clearAllConversations = store.useClearConvoState(); const clearAllConversations = store.useClearConvoState();
const defaultPreset = useRecoilValue(store.defaultPreset); const defaultPreset = useRecoilValue(store.defaultPreset);
const { setConversation } = store.useCreateConversationAtom(index); const { setConversation } = store.useCreateConversationAtom(index);
@ -265,6 +267,12 @@ const useNewConvo = (index = 0) => {
preset = getModelSpecPreset(defaultModelSpec); preset = getModelSpecPreset(defaultModelSpec);
} }
applyModelSpecEffects({
startupConfig,
specName: preset?.spec,
convoId: conversation.conversationId,
});
if (conversation.conversationId === Constants.NEW_CONVO && !modelsData) { if (conversation.conversationId === Constants.NEW_CONVO && !modelsData) {
const filesToDelete = Array.from(files.values()) const filesToDelete = Array.from(files.values())
.filter( .filter(
@ -311,6 +319,7 @@ const useNewConvo = (index = 0) => {
saveBadgesState, saveBadgesState,
pauseGlobalAudio, pauseGlobalAudio,
switchToConversation, switchToConversation,
applyModelSpecEffects,
], ],
); );

View file

@ -16,6 +16,18 @@ export const ephemeralAgentByConvoId = atomFamily<TEphemeralAgent | null, string
] as const, ] as const,
}); });
export function useUpdateEphemeralAgent() {
const updateEphemeralAgent = useRecoilCallback(
({ set }) =>
(convoId: string, agent: TEphemeralAgent | null) => {
set(ephemeralAgentByConvoId(convoId), agent);
},
[],
);
return updateEphemeralAgent;
}
/** /**
* Creates a callback function to apply the ephemeral agent state * Creates a callback function to apply the ephemeral agent state
* from the "new" conversation template to a specified conversation ID. * from the "new" conversation template to a specified conversation ID.

View file

@ -1,4 +1,5 @@
import { import {
Constants,
EModelEndpoint, EModelEndpoint,
defaultEndpoints, defaultEndpoints,
modularEndpoints, modularEndpoints,
@ -176,6 +177,39 @@ export function getConvoSwitchLogic(params: ConversationInitParams): InitiatedTe
}; };
} }
export function getModelSpec({
specName,
startupConfig,
}: {
specName?: string | null;
startupConfig?: t.TStartupConfig;
}): t.TModelSpec | undefined {
if (!startupConfig || !specName) {
return;
}
return startupConfig.modelSpecs?.list?.find((spec) => spec.name === specName);
}
export function applyModelSpecEphemeralAgent({
convoId,
modelSpec,
updateEphemeralAgent,
}: {
convoId?: string | null;
modelSpec?: t.TModelSpec;
updateEphemeralAgent: ((convoId: string, agent: t.TEphemeralAgent | null) => void) | undefined;
}) {
if (!modelSpec || !updateEphemeralAgent) {
return;
}
updateEphemeralAgent((convoId ?? Constants.NEW_CONVO) || Constants.NEW_CONVO, {
mcp: modelSpec.mcpServers ?? [],
web_search: modelSpec.webSearch ?? false,
file_search: modelSpec.fileSearch ?? false,
execute_code: modelSpec.executeCode ?? false,
});
}
/** /**
* Gets default model spec from config and user preferences. * Gets default model spec from config and user preferences.
* Priority: admin default last selected first spec (when prioritize=true or modelSelect disabled). * Priority: admin default last selected first spec (when prioritize=true or modelSelect disabled).

View file

@ -26,6 +26,10 @@ export type TModelSpec = {
showIconInHeader?: boolean; showIconInHeader?: boolean;
iconURL?: string | EModelEndpoint; // Allow using project-included icons iconURL?: string | EModelEndpoint; // Allow using project-included icons
authType?: AuthType; authType?: AuthType;
webSearch?: boolean;
fileSearch?: boolean;
executeCode?: boolean;
mcpServers?: string[];
}; };
export const tModelSpecSchema = z.object({ export const tModelSpecSchema = z.object({
@ -40,6 +44,10 @@ export const tModelSpecSchema = z.object({
showIconInHeader: z.boolean().optional(), showIconInHeader: z.boolean().optional(),
iconURL: z.union([z.string(), eModelEndpointSchema]).optional(), iconURL: z.union([z.string(), eModelEndpointSchema]).optional(),
authType: authTypeSchema.optional(), authType: authTypeSchema.optional(),
webSearch: z.boolean().optional(),
fileSearch: z.boolean().optional(),
executeCode: z.boolean().optional(),
mcpServers: z.array(z.string()).optional(),
}); });
export const specsConfigSchema = z.object({ export const specsConfigSchema = z.object({