diff --git a/client/src/Providers/DragDropContext.tsx b/client/src/Providers/DragDropContext.tsx index 35827c7e96..e5a2177f2d 100644 --- a/client/src/Providers/DragDropContext.tsx +++ b/client/src/Providers/DragDropContext.tsx @@ -1,7 +1,8 @@ import React, { createContext, useContext, useMemo } from 'react'; -import { getEndpointField } from 'librechat-data-provider'; +import { getEndpointField, isAgentsEndpoint } from 'librechat-data-provider'; import type { EModelEndpoint } from 'librechat-data-provider'; -import { useGetEndpointsQuery } from '~/data-provider'; +import { useGetEndpointsQuery, useGetAgentByIdQuery } from '~/data-provider'; +import { useAgentsMapContext } from './AgentsMapContext'; import { useChatContext } from './ChatContext'; interface DragDropContextValue { @@ -9,6 +10,7 @@ interface DragDropContextValue { agentId: string | null | undefined; endpoint: string | null | undefined; endpointType?: EModelEndpoint | undefined; + useResponsesApi?: boolean; } const DragDropContext = createContext(undefined); @@ -16,6 +18,7 @@ const DragDropContext = createContext(undefine export function DragDropProvider({ children }: { children: React.ReactNode }) { const { conversation } = useChatContext(); const { data: endpointsConfig } = useGetEndpointsQuery(); + const agentsMap = useAgentsMapContext(); const endpointType = useMemo(() => { return ( @@ -24,6 +27,34 @@ export function DragDropProvider({ children }: { children: React.ReactNode }) { ); }, [conversation?.endpoint, endpointsConfig]); + const needsAgentFetch = useMemo(() => { + const isAgents = isAgentsEndpoint(conversation?.endpoint); + if (!isAgents || !conversation?.agent_id) { + return false; + } + const agent = agentsMap?.[conversation.agent_id]; + return !agent?.model_parameters; + }, [conversation?.endpoint, conversation?.agent_id, agentsMap]); + + const { data: agentData } = useGetAgentByIdQuery(conversation?.agent_id, { + enabled: needsAgentFetch, + }); + + const useResponsesApi = useMemo(() => { + const isAgents = isAgentsEndpoint(conversation?.endpoint); + if (!isAgents || !conversation?.agent_id || conversation?.useResponsesApi) { + return conversation?.useResponsesApi; + } + const agent = agentData || agentsMap?.[conversation.agent_id]; + return agent?.model_parameters?.useResponsesApi; + }, [ + conversation?.endpoint, + conversation?.agent_id, + conversation?.useResponsesApi, + agentData, + agentsMap, + ]); + /** Context value only created when conversation fields change */ const contextValue = useMemo( () => ({ @@ -31,8 +62,15 @@ export function DragDropProvider({ children }: { children: React.ReactNode }) { agentId: conversation?.agent_id, endpoint: conversation?.endpoint, endpointType: endpointType, + useResponsesApi: useResponsesApi, }), - [conversation?.conversationId, conversation?.agent_id, conversation?.endpoint, endpointType], + [ + conversation?.conversationId, + conversation?.agent_id, + conversation?.endpoint, + useResponsesApi, + endpointType, + ], ); return {children}; diff --git a/client/src/components/Chat/Input/Files/AttachFileChat.tsx b/client/src/components/Chat/Input/Files/AttachFileChat.tsx index 90ac3145bf..37b3584d3e 100644 --- a/client/src/components/Chat/Input/Files/AttachFileChat.tsx +++ b/client/src/components/Chat/Input/Files/AttachFileChat.tsx @@ -10,7 +10,8 @@ import { getEndpointFileConfig, } from 'librechat-data-provider'; import type { TConversation } from 'librechat-data-provider'; -import { useGetFileConfig, useGetEndpointsQuery } from '~/data-provider'; +import { useGetFileConfig, useGetEndpointsQuery, useGetAgentByIdQuery } from '~/data-provider'; +import { useAgentsMapContext } from '~/Providers'; import AttachFileMenu from './AttachFileMenu'; import AttachFile from './AttachFile'; @@ -26,6 +27,28 @@ function AttachFileChat({ const isAgents = useMemo(() => isAgentsEndpoint(endpoint), [endpoint]); const isAssistants = useMemo(() => isAssistantsEndpoint(endpoint), [endpoint]); + const agentsMap = useAgentsMapContext(); + + const needsAgentFetch = useMemo(() => { + if (!isAgents || !conversation?.agent_id) { + return false; + } + const agent = agentsMap?.[conversation.agent_id]; + return !agent?.model_parameters; + }, [isAgents, conversation?.agent_id, agentsMap]); + + const { data: agentData } = useGetAgentByIdQuery(conversation?.agent_id, { + enabled: needsAgentFetch, + }); + + const useResponsesApi = useMemo(() => { + if (!isAgents || !conversation?.agent_id || conversation?.useResponsesApi) { + return conversation?.useResponsesApi; + } + const agent = agentData || agentsMap?.[conversation.agent_id]; + return agent?.model_parameters?.useResponsesApi; + }, [isAgents, conversation?.agent_id, conversation?.useResponsesApi, agentData, agentsMap]); + const { data: fileConfig = null } = useGetFileConfig({ select: (data) => mergeFileConfig(data), }); @@ -68,6 +91,7 @@ function AttachFileChat({ conversationId={conversationId} agentId={conversation?.agent_id} endpointFileConfig={endpointFileConfig} + useResponsesApi={useResponsesApi} /> ); } diff --git a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx index 6e57759e16..4a85c78374 100644 --- a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx +++ b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx @@ -46,6 +46,7 @@ interface AttachFileMenuProps { conversationId: string; endpointType?: EModelEndpoint; endpointFileConfig?: EndpointFileConfig; + useResponsesApi?: boolean; } const AttachFileMenu = ({ @@ -55,6 +56,7 @@ const AttachFileMenu = ({ endpointType, conversationId, endpointFileConfig, + useResponsesApi, }: AttachFileMenuProps) => { const localize = useLocalize(); const isUploadDisabled = disabled ?? false; @@ -117,9 +119,13 @@ const AttachFileMenu = ({ currentProvider = Providers.OPENROUTER; } + const isAzureWithResponsesApi = + currentProvider === EModelEndpoint.azureOpenAI && useResponsesApi; + if ( isDocumentSupportedProvider(endpointType) || - isDocumentSupportedProvider(currentProvider) + isDocumentSupportedProvider(currentProvider) || + isAzureWithResponsesApi ) { items.push({ label: localize('com_ui_upload_provider'), @@ -211,6 +217,7 @@ const AttachFileMenu = ({ provider, endpointType, capabilities, + useResponsesApi, setToolResource, setEphemeralAgent, sharePointEnabled, diff --git a/client/src/components/Chat/Input/Files/DragDropModal.tsx b/client/src/components/Chat/Input/Files/DragDropModal.tsx index 65647a2f22..a59a7e3e9d 100644 --- a/client/src/components/Chat/Input/Files/DragDropModal.tsx +++ b/client/src/components/Chat/Input/Files/DragDropModal.tsx @@ -47,7 +47,7 @@ const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragD * Use definition for agents endpoint for ephemeral agents * */ const capabilities = useAgentCapabilities(agentsConfig?.capabilities ?? defaultAgentCapabilities); - const { conversationId, agentId, endpoint, endpointType } = useDragDropContext(); + const { conversationId, agentId, endpoint, endpointType, useResponsesApi } = useDragDropContext(); const ephemeralAgent = useRecoilValue(ephemeralAgentByConvoId(conversationId ?? '')); const { fileSearchAllowedByAgent, codeAllowedByAgent, provider } = useAgentToolPermissions( agentId, @@ -66,8 +66,15 @@ const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragD /** Helper to get inferred MIME type for a file */ const getFileType = (file: File) => inferMimeType(file.name, file.type); + const isAzureWithResponsesApi = + currentProvider === EModelEndpoint.azureOpenAI && useResponsesApi; + // Check if provider supports document upload - if (isDocumentSupportedProvider(endpointType) || isDocumentSupportedProvider(currentProvider)) { + if ( + isDocumentSupportedProvider(endpointType) || + isDocumentSupportedProvider(currentProvider) || + isAzureWithResponsesApi + ) { const supportsImageDocVideoAudio = currentProvider === EModelEndpoint.google || currentProvider === Providers.OPENROUTER; const validFileTypes = supportsImageDocVideoAudio @@ -130,6 +137,7 @@ const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragD endpoint, endpointType, capabilities, + useResponsesApi, codeAllowedByAgent, fileSearchAllowedByAgent, ]); 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 a9b7139737..d3f0fb65bc 100644 --- a/client/src/components/Chat/Input/Files/__tests__/AttachFileMenu.spec.tsx +++ b/client/src/components/Chat/Input/Files/__tests__/AttachFileMenu.spec.tsx @@ -278,7 +278,6 @@ describe('AttachFileMenu', () => { { name: 'OpenAI', endpoint: EModelEndpoint.openAI }, { name: 'Anthropic', endpoint: EModelEndpoint.anthropic }, { name: 'Google', endpoint: EModelEndpoint.google }, - { name: 'Azure OpenAI', endpoint: EModelEndpoint.azureOpenAI }, { name: 'Custom', endpoint: EModelEndpoint.custom }, ]; @@ -301,6 +300,45 @@ describe('AttachFileMenu', () => { expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); }); }); + + it('should show Upload to Provider for Azure OpenAI with useResponsesApi', () => { + mockUseAgentToolPermissions.mockReturnValue({ + fileSearchAllowedByAgent: false, + codeAllowedByAgent: false, + provider: EModelEndpoint.azureOpenAI, + }); + + renderAttachFileMenu({ + endpoint: EModelEndpoint.azureOpenAI, + endpointType: EModelEndpoint.azureOpenAI, + useResponsesApi: true, + }); + + const button = screen.getByRole('button', { name: /attach file options/i }); + fireEvent.click(button); + + expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); + }); + + it('should NOT show Upload to Provider for Azure OpenAI without useResponsesApi', () => { + mockUseAgentToolPermissions.mockReturnValue({ + fileSearchAllowedByAgent: false, + codeAllowedByAgent: false, + provider: EModelEndpoint.azureOpenAI, + }); + + renderAttachFileMenu({ + endpoint: EModelEndpoint.azureOpenAI, + endpointType: EModelEndpoint.azureOpenAI, + useResponsesApi: false, + }); + + const button = screen.getByRole('button', { name: /attach file options/i }); + fireEvent.click(button); + + expect(screen.queryByText('Upload to Provider')).not.toBeInTheDocument(); + expect(screen.getByText('Upload Image')).toBeInTheDocument(); + }); }); describe('Agent Capabilities', () => { 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 44e632fa12..6def1f3d10 100644 --- a/client/src/components/Chat/Input/Files/__tests__/DragDropModal.spec.tsx +++ b/client/src/components/Chat/Input/Files/__tests__/DragDropModal.spec.tsx @@ -63,7 +63,6 @@ describe('DragDropModal - Provider Detection', () => { { name: 'OpenAI', value: EModelEndpoint.openAI }, { name: 'Anthropic', value: EModelEndpoint.anthropic }, { name: 'Google', value: EModelEndpoint.google }, - { name: 'Azure OpenAI', value: EModelEndpoint.azureOpenAI }, { name: 'Custom', value: EModelEndpoint.custom }, ]; @@ -72,6 +71,10 @@ describe('DragDropModal - Provider Detection', () => { expect(isDocumentSupportedProvider(value)).toBe(true); }); }); + + it('should NOT recognize Azure OpenAI as supported (requires useResponsesApi)', () => { + expect(isDocumentSupportedProvider(EModelEndpoint.azureOpenAI)).toBe(false); + }); }); describe('real-world scenarios', () => { diff --git a/packages/api/src/mcp/oauth/handler.ts b/packages/api/src/mcp/oauth/handler.ts index 0ae9a29292..c07918c591 100644 --- a/packages/api/src/mcp/oauth/handler.ts +++ b/packages/api/src/mcp/oauth/handler.ts @@ -970,8 +970,8 @@ export class MCPOAuthHandler { }); const headers: HeadersInit = { - 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', ...oauthHeaders, }; diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index 7dabc549db..0266ee5109 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -49,7 +49,8 @@ export const documentSupportedProviders = new Set([ EModelEndpoint.anthropic, EModelEndpoint.openAI, EModelEndpoint.custom, - EModelEndpoint.azureOpenAI, + // handled in AttachFileMenu and DragDropModal since azureOpenAI only supports documents with Use Responses API set to true + // EModelEndpoint.azureOpenAI, EModelEndpoint.google, Providers.VERTEXAI, Providers.MISTRALAI, diff --git a/packages/data-provider/src/types/assistants.ts b/packages/data-provider/src/types/assistants.ts index 185df5fa9f..9e1deb20c1 100644 --- a/packages/data-provider/src/types/assistants.ts +++ b/packages/data-provider/src/types/assistants.ts @@ -166,6 +166,7 @@ export type AgentModelParameters = { top_p: AgentParameterValue; frequency_penalty: AgentParameterValue; presence_penalty: AgentParameterValue; + useResponsesApi?: boolean; }; export interface AgentBaseResource {