From 33d6b337bc4d99e5a968a2785ce7ca9be7d489e1 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 27 Oct 2025 19:46:30 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9B=20feat:=20Chat=20Badges=20via=20Mo?= =?UTF-8?q?del=20Specs=20(#10272)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- api/models/Agent.js | 25 +++-- api/server/services/Endpoints/agents/build.js | 4 +- client/src/components/Chat/Input/ChatForm.tsx | 2 + client/src/components/Chat/Input/Mention.tsx | 4 + .../Endpoints/ModelSelectorChatContext.tsx | 13 +-- .../Menus/Endpoints/ModelSelectorContext.tsx | 3 +- client/src/hooks/Agents/index.ts | 1 + .../hooks/Agents/useApplyModelSpecAgents.ts | 95 +++++++++++++++++++ .../Conversations/useNavigateToConvo.tsx | 29 +++++- client/src/hooks/Input/useSelectMention.ts | 6 +- .../hooks/MCP/__tests__/useMCPSelect.test.tsx | 7 +- client/src/hooks/MCP/useMCPSelect.ts | 2 + client/src/hooks/SSE/useEventHandlers.ts | 41 +++++--- client/src/hooks/useNewConvo.ts | 9 ++ client/src/store/agents.ts | 12 +++ client/src/utils/endpoints.ts | 34 +++++++ packages/data-provider/src/models.ts | 8 ++ 17 files changed, 254 insertions(+), 41 deletions(-) create mode 100644 client/src/hooks/Agents/useApplyModelSpecAgents.ts diff --git a/api/models/Agent.js b/api/models/Agent.js index 5468293523..f5f740ba7b 100644 --- a/api/models/Agent.js +++ b/api/models/Agent.js @@ -62,25 +62,37 @@ const getAgents = async (searchParameter) => await Agent.find(searchParameter).l * * @param {Object} params * @param {ServerRequest} params.req + * @param {string} params.spec * @param {string} params.agent_id * @param {string} params.endpoint * @param {import('@librechat/agents').ClientOptions} [params.model_parameters] * @returns {Promise} 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 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} */ const ephemeralAgent = req.body.ephemeralAgent; const mcpServers = new Set(ephemeralAgent?.mcp); + if (modelSpec?.mcpServers) { + for (const mcpServer of modelSpec.mcpServers) { + mcpServers.add(mcpServer); + } + } /** @type {string[]} */ const tools = []; - if (ephemeralAgent?.execute_code === true) { + if (ephemeralAgent?.execute_code === true || modelSpec?.executeCode === true) { tools.push(Tools.execute_code); } - if (ephemeralAgent?.file_search === true) { + if (ephemeralAgent?.file_search === true || modelSpec?.fileSearch === true) { tools.push(Tools.file_search); } - if (ephemeralAgent?.web_search === true) { + if (ephemeralAgent?.web_search === true || modelSpec?.webSearch === true) { tools.push(Tools.web_search); } @@ -122,17 +134,18 @@ const loadEphemeralAgent = async ({ req, agent_id, endpoint, model_parameters: _ * * @param {Object} params * @param {ServerRequest} params.req + * @param {string} params.spec * @param {string} params.agent_id * @param {string} params.endpoint * @param {import('@librechat/agents').ClientOptions} [params.model_parameters] * @returns {Promise} 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) { return null; } 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({ id: agent_id, diff --git a/api/server/services/Endpoints/agents/build.js b/api/server/services/Endpoints/agents/build.js index 3bf90e8d82..34fcaf4be4 100644 --- a/api/server/services/Endpoints/agents/build.js +++ b/api/server/services/Endpoints/agents/build.js @@ -3,9 +3,10 @@ const { isAgentsEndpoint, removeNullishValues, Constants } = require('librechat- const { loadAgent } = require('~/models/Agent'); 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({ req, + spec, agent_id: isAgentsEndpoint(endpoint) ? agent_id : Constants.EPHEMERAL_AGENT_ID, endpoint, model_parameters, @@ -20,7 +21,6 @@ const buildOptions = (req, endpoint, parsedBody, endpointType) => { endpoint, agent_id, endpointType, - instructions, model_parameters, agent: agentPromise, }); diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index 0736c7dc61..b807369082 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -220,6 +220,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
{showPlusPopover && !isAssistantsEndpoint(endpoint) && ( { )} {showMentionPopover && ( ; newConversation: ConvoGenerator; textAreaRef: React.MutableRefObject; @@ -42,6 +45,7 @@ export default function Mention({ const { onSelectMention } = useSelectMention({ presets, modelSpecs, + conversation, assistantsMap, endpointsConfig, newConversation, diff --git a/client/src/components/Chat/Menus/Endpoints/ModelSelectorChatContext.tsx b/client/src/components/Chat/Menus/Endpoints/ModelSelectorChatContext.tsx index bd639523d8..eac3bb200c 100644 --- a/client/src/components/Chat/Menus/Endpoints/ModelSelectorChatContext.tsx +++ b/client/src/components/Chat/Menus/Endpoints/ModelSelectorChatContext.tsx @@ -1,5 +1,5 @@ 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'; interface ModelSelectorChatContextValue { @@ -8,6 +8,7 @@ interface ModelSelectorChatContextValue { spec?: string | null; agent_id?: string | null; assistant_id?: string | null; + conversation: TConversation | null; newConversation: ReturnType['newConversation']; } @@ -26,16 +27,10 @@ export function ModelSelectorChatProvider({ children }: { children: React.ReactN spec: conversation?.spec, agent_id: conversation?.agent_id, assistant_id: conversation?.assistant_id, + conversation, newConversation, }), - [ - conversation?.endpoint, - conversation?.model, - conversation?.spec, - conversation?.agent_id, - conversation?.assistant_id, - newConversation, - ], + [conversation, newConversation], ); return ( diff --git a/client/src/components/Chat/Menus/Endpoints/ModelSelectorContext.tsx b/client/src/components/Chat/Menus/Endpoints/ModelSelectorContext.tsx index a4527d56e7..e79d9a2d21 100644 --- a/client/src/components/Chat/Menus/Endpoints/ModelSelectorContext.tsx +++ b/client/src/components/Chat/Menus/Endpoints/ModelSelectorContext.tsx @@ -57,7 +57,7 @@ export function ModelSelectorProvider({ children, startupConfig }: ModelSelector const agentsMap = useAgentsMapContext(); const assistantsMap = useAssistantsMapContext(); const { data: endpointsConfig } = useGetEndpointsQuery(); - const { endpoint, model, spec, agent_id, assistant_id, newConversation } = + const { endpoint, model, spec, agent_id, assistant_id, conversation, newConversation } = useModelSelectorChatContext(); const modelSpecs = useMemo(() => { const specs = startupConfig?.modelSpecs?.list ?? []; @@ -96,6 +96,7 @@ export function ModelSelectorProvider({ children, startupConfig }: ModelSelector const { onSelectEndpoint, onSelectSpec } = useSelectMention({ // presets, modelSpecs, + conversation, assistantsMap, endpointsConfig, newConversation, diff --git a/client/src/hooks/Agents/index.ts b/client/src/hooks/Agents/index.ts index b0df8398e4..3597b0e646 100644 --- a/client/src/hooks/Agents/index.ts +++ b/client/src/hooks/Agents/index.ts @@ -6,3 +6,4 @@ export { default as useAgentCapabilities } from './useAgentCapabilities'; export { default as useGetAgentsConfig } from './useGetAgentsConfig'; export { default as useAgentDefaultPermissionLevel } from './useAgentDefaultPermissionLevel'; export { default as useAgentToolPermissions } from './useAgentToolPermissions'; +export * from './useApplyModelSpecAgents'; diff --git a/client/src/hooks/Agents/useApplyModelSpecAgents.ts b/client/src/hooks/Agents/useApplyModelSpecAgents.ts new file mode 100644 index 0000000000..e7f15741cb --- /dev/null +++ b/client/src/hooks/Agents/useApplyModelSpecAgents.ts @@ -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; +} diff --git a/client/src/hooks/Conversations/useNavigateToConvo.tsx b/client/src/hooks/Conversations/useNavigateToConvo.tsx index 2bbb4620b3..bfe4a0b96e 100644 --- a/client/src/hooks/Conversations/useNavigateToConvo.tsx +++ b/client/src/hooks/Conversations/useNavigateToConvo.tsx @@ -1,8 +1,14 @@ +import { useCallback } from 'react'; import { useSetRecoilState } from 'recoil'; import { useNavigate } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; 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 { getDefaultEndpoint, clearMessagesCache, @@ -10,15 +16,34 @@ import { getEndpointField, logger, } from '~/utils'; +import { useApplyModelSpecEffects } from '~/hooks/Agents'; import store from '~/store'; const useNavigateToConvo = (index = 0) => { const navigate = useNavigate(); const queryClient = useQueryClient(); const clearAllConversations = store.useClearConvoState(); + const applyModelSpecEffects = useApplyModelSpecEffects(); const setSubmission = useSetRecoilState(store.submissionByIndex(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([QueryKeys.startupConfig]); + applyModelSpecEffects({ + startupConfig, + specName: conversation?.spec, + convoId: conversation.conversationId, + }); + }, + [setConvo, queryClient, applyModelSpecEffects], + ); const fetchFreshData = async (conversation?: Partial) => { const conversationId = conversation?.conversationId; diff --git a/client/src/hooks/Input/useSelectMention.ts b/client/src/hooks/Input/useSelectMention.ts index a5be633da0..51a2f75b11 100644 --- a/client/src/hooks/Input/useSelectMention.ts +++ b/client/src/hooks/Input/useSelectMention.ts @@ -10,18 +10,19 @@ import type { } from 'librechat-data-provider'; import type { MentionOption, ConvoGenerator } from '~/common'; import { getConvoSwitchLogic, getModelSpecIconURL, removeUnavailableTools, logger } from '~/utils'; -import { useChatContext } from '~/Providers'; import { useDefaultConvo } from '~/hooks'; import store from '~/store'; export default function useSelectMention({ presets, modelSpecs, + conversation, assistantsMap, + returnHandlers, endpointsConfig, newConversation, - returnHandlers, }: { + conversation: TConversation | null; presets?: TPreset[]; modelSpecs: TModelSpec[]; assistantsMap?: TAssistantsMap; @@ -29,7 +30,6 @@ export default function useSelectMention({ endpointsConfig: TEndpointsConfig; returnHandlers?: boolean; }) { - const { conversation } = useChatContext(); const getDefaultConversation = useDefaultConvo(); const modularChat = useRecoilValue(store.modularChat); const availableTools = useRecoilValue(store.availableTools); diff --git a/client/src/hooks/MCP/__tests__/useMCPSelect.test.tsx b/client/src/hooks/MCP/__tests__/useMCPSelect.test.tsx index 7145e95e74..ab10ec6d76 100644 --- a/client/src/hooks/MCP/__tests__/useMCPSelect.test.tsx +++ b/client/src/hooks/MCP/__tests__/useMCPSelect.test.tsx @@ -431,9 +431,10 @@ describe('useMCPSelect', () => { }); }); - // Values should remain unchanged since empty mcp array doesn't trigger update - // (due to the condition: ephemeralAgent?.mcp && ephemeralAgent.mcp.length > 0) - expect(result.current.mcpHook.mcpValues).toEqual(['initial-value']); + // Values should sync to empty array when ephemeralAgent.mcp is set to [] + await waitFor(() => { + expect(result.current.mcpHook.mcpValues).toEqual([]); + }); }); it('should properly sync non-empty arrays from ephemeralAgent', async () => { diff --git a/client/src/hooks/MCP/useMCPSelect.ts b/client/src/hooks/MCP/useMCPSelect.ts index 3f37bb4d70..3ce7999346 100644 --- a/client/src/hooks/MCP/useMCPSelect.ts +++ b/client/src/hooks/MCP/useMCPSelect.ts @@ -25,6 +25,8 @@ export function useMCPSelect({ conversationId }: { conversationId?: string | nul // Strip out servers that are not available in the startup config const activeMcps = mcps.filter((mcp) => configuredServers.has(mcp)); setMCPValuesRaw(activeMcps); + } else { + setMCPValuesRaw([]); } }, [ephemeralAgent?.mcp, setMCPValuesRaw, configuredServers]); diff --git a/client/src/hooks/SSE/useEventHandlers.ts b/client/src/hooks/SSE/useEventHandlers.ts index 83c1ff1ad9..6348581b68 100644 --- a/client/src/hooks/SSE/useEventHandlers.ts +++ b/client/src/hooks/SSE/useEventHandlers.ts @@ -1,8 +1,8 @@ -import { v4 } from 'uuid'; import { useCallback, useRef } from 'react'; +import { v4 } from 'uuid'; import { useSetRecoilState } from 'recoil'; -import { useParams, useNavigate, useLocation } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; +import { useParams, useNavigate, useLocation } from 'react-router-dom'; import { QueryKeys, Constants, @@ -13,7 +13,12 @@ import { tConvoUpdateSchema, isAssistantsEndpoint, } 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 { InfiniteData } from '@tanstack/react-query'; import type { TGenTitleMutation } from '~/data-provider'; @@ -31,11 +36,12 @@ import { } from '~/utils'; import useAttachmentHandler from '~/hooks/SSE/useAttachmentHandler'; import useContentHandler from '~/hooks/SSE/useContentHandler'; -import store, { useApplyNewAgentTemplate } from '~/store'; import useStepHandler from '~/hooks/SSE/useStepHandler'; +import { useApplyAgentTemplate } from '~/hooks/Agents'; import { useAuthContext } from '~/hooks/AuthContext'; import { MESSAGE_UPDATE_INTERVAL } from '~/common'; import { useLiveAnnouncer } from '~/Providers'; +import store from '~/store'; type TSyncData = { sync: boolean; @@ -172,7 +178,7 @@ export default function useEventHandlers({ }: EventHandlerParams) { const queryClient = useQueryClient(); const { announcePolite } = useLiveAnnouncer(); - const applyAgentTemplate = useApplyNewAgentTemplate(); + const applyAgentTemplate = useApplyAgentTemplate(); const setAbortScroll = useSetRecoilState(store.abortScroll); const navigate = useNavigate(); const location = useLocation(); @@ -356,6 +362,7 @@ export default function useEventHandlers({ const createdHandler = useCallback( (data: TResData, submission: EventSubmission) => { + queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]); const { messages, userMessage, isRegenerate = false, isTemporary = false } = submission; const initialResponse = { ...submission.initialResponse, @@ -411,11 +418,13 @@ export default function useEventHandlers({ } if (conversationId) { - applyAgentTemplate( - conversationId, - submission.conversation.conversationId, - submission.ephemeralAgent, - ); + applyAgentTemplate({ + targetId: conversationId, + sourceId: submission.conversation?.conversationId, + ephemeralAgent: submission.ephemeralAgent, + specName: submission.conversation?.spec, + startupConfig: queryClient.getQueryData([QueryKeys.startupConfig]), + }); } if (resetLatestMessage) { @@ -566,11 +575,13 @@ export default function useEventHandlers({ }); if (conversation.conversationId && submission.ephemeralAgent) { - applyAgentTemplate( - conversation.conversationId, - submissionConvo.conversationId, - submission.ephemeralAgent, - ); + applyAgentTemplate({ + targetId: conversation.conversationId, + sourceId: submissionConvo.conversationId, + ephemeralAgent: submission.ephemeralAgent, + specName: submission.conversation?.spec, + startupConfig: queryClient.getQueryData([QueryKeys.startupConfig]), + }); } if (location.pathname === `/c/${Constants.NEW_CONVO}`) { diff --git a/client/src/hooks/useNewConvo.ts b/client/src/hooks/useNewConvo.ts index 63b442b83a..9f0e17b297 100644 --- a/client/src/hooks/useNewConvo.ts +++ b/client/src/hooks/useNewConvo.ts @@ -29,6 +29,7 @@ import { import { useDeleteFilesMutation, useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider'; import useAssistantListMap from './Assistants/useAssistantListMap'; import { useResetChatBadges } from './useChatBadges'; +import { useApplyModelSpecEffects } from './Agents'; import { usePauseGlobalAudio } from './Audio'; import { logger } from '~/utils'; import store from '~/store'; @@ -37,6 +38,7 @@ const useNewConvo = (index = 0) => { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const { data: startupConfig } = useGetStartupConfig(); + const applyModelSpecEffects = useApplyModelSpecEffects(); const clearAllConversations = store.useClearConvoState(); const defaultPreset = useRecoilValue(store.defaultPreset); const { setConversation } = store.useCreateConversationAtom(index); @@ -265,6 +267,12 @@ const useNewConvo = (index = 0) => { preset = getModelSpecPreset(defaultModelSpec); } + applyModelSpecEffects({ + startupConfig, + specName: preset?.spec, + convoId: conversation.conversationId, + }); + if (conversation.conversationId === Constants.NEW_CONVO && !modelsData) { const filesToDelete = Array.from(files.values()) .filter( @@ -311,6 +319,7 @@ const useNewConvo = (index = 0) => { saveBadgesState, pauseGlobalAudio, switchToConversation, + applyModelSpecEffects, ], ); diff --git a/client/src/store/agents.ts b/client/src/store/agents.ts index a62fae6046..13136ef34e 100644 --- a/client/src/store/agents.ts +++ b/client/src/store/agents.ts @@ -16,6 +16,18 @@ export const ephemeralAgentByConvoId = atomFamily + (convoId: string, agent: TEphemeralAgent | null) => { + set(ephemeralAgentByConvoId(convoId), agent); + }, + [], + ); + + return updateEphemeralAgent; +} + /** * Creates a callback function to apply the ephemeral agent state * from the "new" conversation template to a specified conversation ID. diff --git a/client/src/utils/endpoints.ts b/client/src/utils/endpoints.ts index c98680843a..1de9e2845c 100644 --- a/client/src/utils/endpoints.ts +++ b/client/src/utils/endpoints.ts @@ -1,4 +1,5 @@ import { + Constants, EModelEndpoint, defaultEndpoints, 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. * Priority: admin default → last selected → first spec (when prioritize=true or modelSelect disabled). diff --git a/packages/data-provider/src/models.ts b/packages/data-provider/src/models.ts index 78ba1237fc..1edca6ea37 100644 --- a/packages/data-provider/src/models.ts +++ b/packages/data-provider/src/models.ts @@ -26,6 +26,10 @@ export type TModelSpec = { showIconInHeader?: boolean; iconURL?: string | EModelEndpoint; // Allow using project-included icons authType?: AuthType; + webSearch?: boolean; + fileSearch?: boolean; + executeCode?: boolean; + mcpServers?: string[]; }; export const tModelSpecSchema = z.object({ @@ -40,6 +44,10 @@ export const tModelSpecSchema = z.object({ showIconInHeader: z.boolean().optional(), iconURL: z.union([z.string(), eModelEndpointSchema]).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({