From 030dc98a1d4ee931504d3fd22694c4859dde0542 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 10 May 2026 17:47:05 -0400 Subject: [PATCH] =?UTF-8?q?=E2=98=81=EF=B8=8F=20fix:=20Enable=20Azure=20Ag?= =?UTF-8?q?ent=20Provider=20Uploads=20(#13045)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/server/controllers/agents/v1.js | 5 ++ api/server/controllers/agents/v1.spec.js | 29 +++++++++++ client/src/Providers/DragDropContext.tsx | 11 +++-- .../__tests__/DragDropContext.spec.tsx | 49 +++++++++++++++++++ .../Chat/Input/Files/AttachFileChat.tsx | 11 +++-- .../Chat/Input/Files/AttachFileMenu.tsx | 4 +- .../Chat/Input/Files/DragDropModal.tsx | 4 +- .../Files/__tests__/AttachFileChat.spec.tsx | 45 +++++++++++++++++ .../Files/__tests__/AttachFileMenu.spec.tsx | 11 +++++ .../Files/__tests__/DragDropModal.spec.tsx | 19 +++++++ 10 files changed, 176 insertions(+), 12 deletions(-) diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index 012eab36c0..536044e5cc 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -55,6 +55,10 @@ const systemTools = { const MAX_SEARCH_LEN = 100; const escapeRegex = (str = '') => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +const getSafeModelParameters = (modelParameters) => { + const { useResponsesApi } = modelParameters ?? {}; + return typeof useResponsesApi === 'boolean' ? { useResponsesApi } : {}; +}; /** * Looks up each referenced agent id in Mongo, splits them into three @@ -458,6 +462,7 @@ const getAgentHandler = async (req, res, expandProperties = false) => { author: agent.author, provider: agent.provider, model: agent.model, + model_parameters: getSafeModelParameters(agent.model_parameters), isPublic: agent.isPublic, version: agent.version, // Safe metadata diff --git a/api/server/controllers/agents/v1.spec.js b/api/server/controllers/agents/v1.spec.js index 81f72264bd..17904ad3fd 100644 --- a/api/server/controllers/agents/v1.spec.js +++ b/api/server/controllers/agents/v1.spec.js @@ -72,6 +72,7 @@ jest.mock('~/cache', () => ({ const { createAgent: createAgentHandler, + getAgent: getAgentHandler, duplicateAgent: duplicateAgentHandler, revertAgentVersion: revertAgentVersionHandler, updateAgent: updateAgentHandler, @@ -483,6 +484,34 @@ describe('Agent Controllers - Mass Assignment Protection', () => { }); }); + describe('getAgentHandler', () => { + test('should return the safe Responses API flag in the basic VIEW response', async () => { + const agent = await Agent.create({ + id: `agent_${uuidv4()}`, + name: 'Azure Agent', + description: 'Uses Responses API', + provider: 'azureOpenAI', + model: 'gpt-5.5', + author: mockReq.user.id, + model_parameters: { + useResponsesApi: true, + temperature: 0.7, + apiKey: 'secret-value', + }, + }); + + mockReq.params = { id: agent.id }; + + await getAgentHandler(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(200); + const response = mockRes.json.mock.calls[0][0]; + expect(response.model_parameters).toEqual({ useResponsesApi: true }); + expect(response.model_parameters.temperature).toBeUndefined(); + expect(response.model_parameters.apiKey).toBeUndefined(); + }); + }); + describe('updateAgentHandler', () => { let existingAgentId; let existingAgentAuthorId; diff --git a/client/src/Providers/DragDropContext.tsx b/client/src/Providers/DragDropContext.tsx index ed4dddb58f..eae5f0165f 100644 --- a/client/src/Providers/DragDropContext.tsx +++ b/client/src/Providers/DragDropContext.tsx @@ -38,8 +38,7 @@ export function DragDropProvider({ children }: { children: React.ReactNode }) { if (!isAgents || !conversation?.agent_id) { return undefined; } - const agent = agentData || agentsMap?.[conversation.agent_id]; - return agent?.provider; + return agentData?.provider ?? agentsMap?.[conversation.agent_id]?.provider; }, [conversation?.endpoint, conversation?.agent_id, agentData, agentsMap]); const endpointType = useMemo( @@ -49,11 +48,13 @@ export function DragDropProvider({ children }: { children: React.ReactNode }) { const useResponsesApi = useMemo(() => { const isAgents = isAgentsEndpoint(conversation?.endpoint); - if (!isAgents || !conversation?.agent_id || conversation?.useResponsesApi) { + if (!isAgents || !conversation?.agent_id || conversation?.useResponsesApi !== undefined) { return conversation?.useResponsesApi; } - const agent = agentData || agentsMap?.[conversation.agent_id]; - return agent?.model_parameters?.useResponsesApi; + return ( + agentData?.model_parameters?.useResponsesApi ?? + agentsMap?.[conversation.agent_id]?.model_parameters?.useResponsesApi + ); }, [ conversation?.endpoint, conversation?.agent_id, diff --git a/client/src/Providers/__tests__/DragDropContext.spec.tsx b/client/src/Providers/__tests__/DragDropContext.spec.tsx index 3c5e0f0796..12a5edbb0a 100644 --- a/client/src/Providers/__tests__/DragDropContext.spec.tsx +++ b/client/src/Providers/__tests__/DragDropContext.spec.tsx @@ -100,6 +100,55 @@ describe('DragDropContext endpointType resolution', () => { const { result } = renderHook(() => useDragDropContext(), { wrapper }); expect(result.current.endpointType).toBe(EModelEndpoint.custom); }); + + it('falls back to agentsMap provider when agentData omits provider', () => { + mockConversation = { endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }; + mockAgentsMap = { + 'agent-1': { provider: EModelEndpoint.openAI, model_parameters: {} } as Partial, + }; + mockAgentQueryData = {} as Partial; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.endpointType).toBe(EModelEndpoint.openAI); + }); + }); + + describe('useResponsesApi resolution for agents', () => { + it('uses fetched agent model parameters when conversation does not override them', () => { + mockConversation = { endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }; + mockAgentQueryData = { + provider: EModelEndpoint.azureOpenAI, + model_parameters: { useResponsesApi: true }, + } as Partial; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.useResponsesApi).toBe(true); + }); + + it('falls back to agentsMap model parameters when fetched agent omits them', () => { + mockConversation = { endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }; + mockAgentsMap = { + 'agent-1': { + provider: EModelEndpoint.azureOpenAI, + model_parameters: { useResponsesApi: true }, + } as Partial, + }; + mockAgentQueryData = { provider: EModelEndpoint.azureOpenAI } as Partial; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.useResponsesApi).toBe(true); + }); + + it('preserves an explicit conversation useResponsesApi false override', () => { + mockConversation = { + endpoint: EModelEndpoint.agents, + agent_id: 'agent-1', + useResponsesApi: false, + }; + mockAgentQueryData = { + provider: EModelEndpoint.azureOpenAI, + model_parameters: { useResponsesApi: true }, + } as Partial; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.useResponsesApi).toBe(false); + }); }); describe('agents endpoint without provider', () => { diff --git a/client/src/components/Chat/Input/Files/AttachFileChat.tsx b/client/src/components/Chat/Input/Files/AttachFileChat.tsx index 7eb9b0c474..89bd49f0ae 100644 --- a/client/src/components/Chat/Input/Files/AttachFileChat.tsx +++ b/client/src/components/Chat/Input/Files/AttachFileChat.tsx @@ -48,11 +48,13 @@ function AttachFileChat({ }); const useResponsesApi = useMemo(() => { - if (!isAgents || !conversation?.agent_id || conversation?.useResponsesApi) { + if (!isAgents || !conversation?.agent_id || conversation?.useResponsesApi !== undefined) { return conversation?.useResponsesApi; } - const agent = agentData || agentsMap?.[conversation.agent_id]; - return agent?.model_parameters?.useResponsesApi; + return ( + agentData?.model_parameters?.useResponsesApi ?? + agentsMap?.[conversation.agent_id]?.model_parameters?.useResponsesApi + ); }, [isAgents, conversation?.agent_id, conversation?.useResponsesApi, agentData, agentsMap]); const { data: fileConfig = null } = useGetFileConfig({ @@ -65,8 +67,7 @@ function AttachFileChat({ if (!isAgents || !conversation?.agent_id) { return undefined; } - const agent = agentData || agentsMap?.[conversation.agent_id]; - return agent?.provider; + return agentData?.provider ?? agentsMap?.[conversation.agent_id]?.provider; }, [isAgents, conversation?.agent_id, agentData, agentsMap]); const endpointType = useMemo( diff --git a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx index cb1c5e53d7..4ee4e123ea 100644 --- a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx +++ b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx @@ -157,7 +157,9 @@ const AttachFileMenu = ({ } const isAzureWithResponsesApi = - currentProvider === EModelEndpoint.azureOpenAI && useResponsesApi; + (currentProvider === EModelEndpoint.azureOpenAI || + endpointType === EModelEndpoint.azureOpenAI) && + useResponsesApi === true; if ( isDocumentSupportedProvider(endpointType) || diff --git a/client/src/components/Chat/Input/Files/DragDropModal.tsx b/client/src/components/Chat/Input/Files/DragDropModal.tsx index cb5109c866..295f48cf12 100644 --- a/client/src/components/Chat/Input/Files/DragDropModal.tsx +++ b/client/src/components/Chat/Input/Files/DragDropModal.tsx @@ -68,7 +68,9 @@ const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragD const getFileType = (file: File) => inferMimeType(file.name, file.type); const isAzureWithResponsesApi = - currentProvider === EModelEndpoint.azureOpenAI && useResponsesApi; + (currentProvider === EModelEndpoint.azureOpenAI || + endpointType === EModelEndpoint.azureOpenAI) && + useResponsesApi === true; // Check if provider supports document upload if ( diff --git a/client/src/components/Chat/Input/Files/__tests__/AttachFileChat.spec.tsx b/client/src/components/Chat/Input/Files/__tests__/AttachFileChat.spec.tsx index 80f06a1b89..e2c5a27613 100644 --- a/client/src/components/Chat/Input/Files/__tests__/AttachFileChat.spec.tsx +++ b/client/src/components/Chat/Input/Files/__tests__/AttachFileChat.spec.tsx @@ -128,6 +128,51 @@ describe('AttachFileChat', () => { renderComponent({ endpoint: EModelEndpoint.agents, agent_id: 'agent-2' }); expect(mockAttachFileMenuProps.endpointType).toBe(EModelEndpoint.custom); }); + + it('falls back to agentsMap provider when fetched agent omits provider', () => { + mockAgentsMap = { + 'agent-1': { provider: EModelEndpoint.openAI, model_parameters: {} } as Partial, + }; + mockAgentQueryData = {} as Partial; + renderComponent({ endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }); + expect(mockAttachFileMenuProps.endpointType).toBe(EModelEndpoint.openAI); + }); + }); + + describe('useResponsesApi resolution for agents', () => { + it('passes useResponsesApi from fetched agent model parameters', () => { + mockAgentQueryData = { + provider: EModelEndpoint.azureOpenAI, + model_parameters: { useResponsesApi: true }, + } as Partial; + renderComponent({ endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }); + expect(mockAttachFileMenuProps.useResponsesApi).toBe(true); + }); + + it('falls back to agentsMap model parameters when fetched agent omits them', () => { + mockAgentsMap = { + 'agent-1': { + provider: EModelEndpoint.azureOpenAI, + model_parameters: { useResponsesApi: true }, + } as Partial, + }; + mockAgentQueryData = { provider: EModelEndpoint.azureOpenAI } as Partial; + renderComponent({ endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }); + expect(mockAttachFileMenuProps.useResponsesApi).toBe(true); + }); + + it('preserves an explicit conversation useResponsesApi false override', () => { + mockAgentQueryData = { + provider: EModelEndpoint.azureOpenAI, + model_parameters: { useResponsesApi: true }, + } as Partial; + renderComponent({ + endpoint: EModelEndpoint.agents, + agent_id: 'agent-1', + useResponsesApi: false, + }); + expect(mockAttachFileMenuProps.useResponsesApi).toBe(false); + }); }); describe('endpointType resolution for non-agents', () => { diff --git a/client/src/components/Chat/Input/Files/__tests__/AttachFileMenu.spec.tsx b/client/src/components/Chat/Input/Files/__tests__/AttachFileMenu.spec.tsx index 35b5f723c1..d6a3c995c5 100644 --- a/client/src/components/Chat/Input/Files/__tests__/AttachFileMenu.spec.tsx +++ b/client/src/components/Chat/Input/Files/__tests__/AttachFileMenu.spec.tsx @@ -203,6 +203,17 @@ describe('AttachFileMenu', () => { expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); }); + it('shows "Upload to Provider" for azureOpenAI endpointType with useResponsesApi', () => { + setupMocks(); + renderMenu({ + endpoint: EModelEndpoint.agents, + endpointType: EModelEndpoint.azureOpenAI, + useResponsesApi: true, + }); + openMenu(); + expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); + }); + it('shows "Upload Image" for azureOpenAI without useResponsesApi', () => { setupMocks({ provider: EModelEndpoint.azureOpenAI }); renderMenu({ endpointType: EModelEndpoint.azureOpenAI, useResponsesApi: false }); diff --git a/client/src/components/Chat/Input/Files/__tests__/DragDropModal.spec.tsx b/client/src/components/Chat/Input/Files/__tests__/DragDropModal.spec.tsx index 6def1f3d10..71d91a197b 100644 --- a/client/src/components/Chat/Input/Files/__tests__/DragDropModal.spec.tsx +++ b/client/src/components/Chat/Input/Files/__tests__/DragDropModal.spec.tsx @@ -124,6 +124,25 @@ describe('DragDropModal - Provider Detection', () => { isDocumentSupportedProvider(scenario.currentProvider), ).toBe(true); }); + + it('should handle Azure OpenAI endpointType when Responses API is enabled', () => { + const scenario = { + currentProvider: EModelEndpoint.agents, + endpointType: EModelEndpoint.azureOpenAI, + useResponsesApi: true, + }; + + const isAzureWithResponsesApi = + (scenario.currentProvider === EModelEndpoint.azureOpenAI || + scenario.endpointType === EModelEndpoint.azureOpenAI) && + scenario.useResponsesApi === true; + + expect( + isDocumentSupportedProvider(scenario.endpointType) || + isDocumentSupportedProvider(scenario.currentProvider) || + isAzureWithResponsesApi, + ).toBe(true); + }); }); describe('HEIC/HEIF file type inference', () => {